This is the abridged developer documentation for Effect. # Start of Effect documentation # [Batching](https://effect.website/docs/batching/) ## Overview import { Aside } from "@astrojs/starlight/components" In typical application development, when interacting with external APIs, databases, or other data sources, we often define functions that perform requests and handle their results or failures accordingly. ### Simple Model Setup Here's a basic model that outlines the structure of our data and possible errors: ```ts twoslash // ------------------------------ // Model // ------------------------------ interface User { readonly _tag: "User" readonly id: number readonly name: string readonly email: string } class GetUserError { readonly _tag = "GetUserError" } interface Todo { readonly _tag: "Todo" readonly id: number readonly message: string readonly ownerId: number } class GetTodosError { readonly _tag = "GetTodosError" } class SendEmailError { readonly _tag = "SendEmailError" } ``` ### Defining API Functions Let's define functions that interact with an external API, handling common operations such as fetching todos, retrieving user details, and sending emails. ```ts twoslash collapse={7-31} import { Effect } from "effect" // ------------------------------ // Model // ------------------------------ interface User { readonly _tag: "User" readonly id: number readonly name: string readonly email: string } class GetUserError { readonly _tag = "GetUserError" } interface Todo { readonly _tag: "Todo" readonly id: number readonly message: string readonly ownerId: number } class GetTodosError { readonly _tag = "GetTodosError" } class SendEmailError { readonly _tag = "SendEmailError" } // ------------------------------ // API // ------------------------------ // Fetches a list of todos from an external API const getTodos = Effect.tryPromise({ try: () => fetch("https://api.example.demo/todos").then( (res) => res.json() as Promise> ), catch: () => new GetTodosError() }) // Retrieves a user by their ID from an external API const getUserById = (id: number) => Effect.tryPromise({ try: () => fetch(`https://api.example.demo/getUserById?id=${id}`).then( (res) => res.json() as Promise ), catch: () => new GetUserError() }) // Sends an email via an external API const sendEmail = (address: string, text: string) => Effect.tryPromise({ try: () => fetch("https://api.example.demo/sendEmail", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ address, text }) }).then((res) => res.json() as Promise), catch: () => new SendEmailError() }) // Sends an email to a user by fetching their details first const sendEmailToUser = (id: number, message: string) => getUserById(id).pipe( Effect.andThen((user) => sendEmail(user.email, message)) ) // Notifies the owner of a todo by sending them an email const notifyOwner = (todo: Todo) => getUserById(todo.ownerId).pipe( Effect.andThen((user) => sendEmailToUser(user.id, `hey ${user.name} you got a todo!`) ) ) ``` While this approach is straightforward and readable, it may not be the most efficient. Repeated API calls, especially when many todos share the same owner, can significantly increase network overhead and slow down your application. ### Using the API Functions While these functions are clear and easy to understand, their use may not be the most efficient. For example, notifying todo owners involves repeated API calls which can be optimized. ```ts twoslash collapse={7-31,37-82} import { Effect } from "effect" // ------------------------------ // Model // ------------------------------ interface User { readonly _tag: "User" readonly id: number readonly name: string readonly email: string } class GetUserError { readonly _tag = "GetUserError" } interface Todo { readonly _tag: "Todo" readonly id: number readonly message: string readonly ownerId: number } class GetTodosError { readonly _tag = "GetTodosError" } class SendEmailError { readonly _tag = "SendEmailError" } // ------------------------------ // API // ------------------------------ // Fetches a list of todos from an external API const getTodos = Effect.tryPromise({ try: () => fetch("https://api.example.demo/todos").then( (res) => res.json() as Promise> ), catch: () => new GetTodosError() }) // Retrieves a user by their ID from an external API const getUserById = (id: number) => Effect.tryPromise({ try: () => fetch(`https://api.example.demo/getUserById?id=${id}`).then( (res) => res.json() as Promise ), catch: () => new GetUserError() }) // Sends an email via an external API const sendEmail = (address: string, text: string) => Effect.tryPromise({ try: () => fetch("https://api.example.demo/sendEmail", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ address, text }) }).then((res) => res.json() as Promise), catch: () => new SendEmailError() }) // Sends an email to a user by fetching their details first const sendEmailToUser = (id: number, message: string) => getUserById(id).pipe( Effect.andThen((user) => sendEmail(user.email, message)) ) // Notifies the owner of a todo by sending them an email const notifyOwner = (todo: Todo) => getUserById(todo.ownerId).pipe( Effect.andThen((user) => sendEmailToUser(user.id, `hey ${user.name} you got a todo!`) ) ) // Orchestrates operations on todos, notifying their owners const program = Effect.gen(function* () { const todos = yield* getTodos yield* Effect.forEach(todos, (todo) => notifyOwner(todo), { concurrency: "unbounded" }) }) ``` This implementation performs an API call for each todo to fetch the owner's details and send an email. If multiple todos have the same owner, this results in redundant API calls. ## Batching Let's assume that `getUserById` and `sendEmail` can be batched. This means that we can send multiple requests in a single HTTP call, reducing the number of API requests and improving performance. **Step-by-Step Guide to Batching** 1. **Declaring Requests:** We'll start by transforming our requests into structured data models. This involves detailing input parameters, expected outputs, and possible errors. Structuring requests this way not only helps in efficiently managing data but also in comparing different requests to understand if they refer to the same input parameters. 2. **Declaring Resolvers:** Resolvers are designed to handle multiple requests simultaneously. By leveraging the ability to compare requests (ensuring they refer to the same input parameters), resolvers can execute several requests in one go, maximizing the utility of batching. 3. **Defining Queries:** Finally, we'll define queries that utilize these batch-resolvers to perform operations. This step ties together the structured requests and their corresponding resolvers into functional components of the application. ### Declaring Requests We'll design a model using the concept of a `Request` that a data source might support: ```ts showLineNumbers=false Request ``` A `Request` is a construct representing a request for a value of type `Value`, which might fail with an error of type `Error`. Let's start by defining a structured model for the types of requests our data sources can handle. ```ts twoslash collapse={7-31} import { Request } from "effect" // ------------------------------ // Model // ------------------------------ interface User { readonly _tag: "User" readonly id: number readonly name: string readonly email: string } class GetUserError { readonly _tag = "GetUserError" } interface Todo { readonly _tag: "Todo" readonly id: number readonly message: string readonly ownerId: number } class GetTodosError { readonly _tag = "GetTodosError" } class SendEmailError { readonly _tag = "SendEmailError" } // ------------------------------ // Requests // ------------------------------ // Define a request to get multiple Todo items which might // fail with a GetTodosError interface GetTodos extends Request.Request, GetTodosError> { readonly _tag: "GetTodos" } // Create a tagged constructor for GetTodos requests const GetTodos = Request.tagged("GetTodos") // Define a request to fetch a User by ID which might // fail with a GetUserError interface GetUserById extends Request.Request { readonly _tag: "GetUserById" readonly id: number } // Create a tagged constructor for GetUserById requests const GetUserById = Request.tagged("GetUserById") // Define a request to send an email which might // fail with a SendEmailError interface SendEmail extends Request.Request { readonly _tag: "SendEmail" readonly address: string readonly text: string } // Create a tagged constructor for SendEmail requests const SendEmail = Request.tagged("SendEmail") ``` Each request is defined with a specific data structure that extends from a generic `Request` type, ensuring that each request carries its unique data requirements along with a specific error type. By using tagged constructors like `Request.tagged`, we can easily instantiate request objects that are recognizable and manageable throughout the application. ### Declaring Resolvers After defining our requests, the next step is configuring how Effect resolves these requests using `RequestResolver`: ```ts showLineNumbers=false RequestResolver ``` A `RequestResolver` requires an environment `R` and is capable of executing requests of type `A`. In this section, we'll create individual resolvers for each type of request. The granularity of your resolvers can vary, but typically, they are divided based on the batching capabilities of the corresponding API calls. ```ts twoslash collapse={7-31,37-65} import { Effect, Request, RequestResolver } from "effect" // ------------------------------ // Model // ------------------------------ interface User { readonly _tag: "User" readonly id: number readonly name: string readonly email: string } class GetUserError { readonly _tag = "GetUserError" } interface Todo { readonly _tag: "Todo" readonly id: number readonly message: string readonly ownerId: number } class GetTodosError { readonly _tag = "GetTodosError" } class SendEmailError { readonly _tag = "SendEmailError" } // ------------------------------ // Requests // ------------------------------ // Define a request to get multiple Todo items which might // fail with a GetTodosError interface GetTodos extends Request.Request, GetTodosError> { readonly _tag: "GetTodos" } // Create a tagged constructor for GetTodos requests const GetTodos = Request.tagged("GetTodos") // Define a request to fetch a User by ID which might // fail with a GetUserError interface GetUserById extends Request.Request { readonly _tag: "GetUserById" readonly id: number } // Create a tagged constructor for GetUserById requests const GetUserById = Request.tagged("GetUserById") // Define a request to send an email which might // fail with a SendEmailError interface SendEmail extends Request.Request { readonly _tag: "SendEmail" readonly address: string readonly text: string } // Create a tagged constructor for SendEmail requests const SendEmail = Request.tagged("SendEmail") // ------------------------------ // Resolvers // ------------------------------ // Assuming GetTodos cannot be batched, we create a standard resolver const GetTodosResolver = RequestResolver.fromEffect( (_: GetTodos): Effect.Effect => Effect.tryPromise({ try: () => fetch("https://api.example.demo/todos").then( (res) => res.json() as Promise> ), catch: () => new GetTodosError() }) ) // Assuming GetUserById can be batched, we create a batched resolver const GetUserByIdResolver = RequestResolver.makeBatched( (requests: ReadonlyArray) => Effect.tryPromise({ try: () => fetch("https://api.example.demo/getUserByIdBatch", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ users: requests.map(({ id }) => ({ id })) }) }).then((res) => res.json()) as Promise>, catch: () => new GetUserError() }).pipe( Effect.andThen((users) => Effect.forEach(requests, (request, index) => Request.completeEffect(request, Effect.succeed(users[index]!)) ) ), Effect.catchAll((error) => Effect.forEach(requests, (request) => Request.completeEffect(request, Effect.fail(error)) ) ) ) ) // Assuming SendEmail can be batched, we create a batched resolver const SendEmailResolver = RequestResolver.makeBatched( (requests: ReadonlyArray) => Effect.tryPromise({ try: () => fetch("https://api.example.demo/sendEmailBatch", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ emails: requests.map(({ address, text }) => ({ address, text })) }) }).then((res) => res.json() as Promise), catch: () => new SendEmailError() }).pipe( Effect.andThen( Effect.forEach(requests, (request) => Request.completeEffect(request, Effect.void) ) ), Effect.catchAll((error) => Effect.forEach(requests, (request) => Request.completeEffect(request, Effect.fail(error)) ) ) ) ) ``` In this configuration: - **GetTodosResolver** handles the fetching of multiple `Todo` items. It's set up as a standard resolver since we assume it cannot be batched. - **GetUserByIdResolver** and **SendEmailResolver** are configured as batched resolvers. This setup is based on the assumption that these requests can be processed in batches, enhancing performance and reducing the number of API calls. ### Defining Queries Now that we've set up our resolvers, we're ready to tie all the pieces together to define our This step will enable us to perform data operations effectively within our application. ```ts twoslash collapse={7-31,37-65,71-142} import { Effect, Request, RequestResolver } from "effect" // ------------------------------ // Model // ------------------------------ interface User { readonly _tag: "User" readonly id: number readonly name: string readonly email: string } class GetUserError { readonly _tag = "GetUserError" } interface Todo { readonly _tag: "Todo" readonly id: number readonly message: string readonly ownerId: number } class GetTodosError { readonly _tag = "GetTodosError" } class SendEmailError { readonly _tag = "SendEmailError" } // ------------------------------ // Requests // ------------------------------ // Define a request to get multiple Todo items which might // fail with a GetTodosError interface GetTodos extends Request.Request, GetTodosError> { readonly _tag: "GetTodos" } // Create a tagged constructor for GetTodos requests const GetTodos = Request.tagged("GetTodos") // Define a request to fetch a User by ID which might // fail with a GetUserError interface GetUserById extends Request.Request { readonly _tag: "GetUserById" readonly id: number } // Create a tagged constructor for GetUserById requests const GetUserById = Request.tagged("GetUserById") // Define a request to send an email which might // fail with a SendEmailError interface SendEmail extends Request.Request { readonly _tag: "SendEmail" readonly address: string readonly text: string } // Create a tagged constructor for SendEmail requests const SendEmail = Request.tagged("SendEmail") // ------------------------------ // Resolvers // ------------------------------ // Assuming GetTodos cannot be batched, we create a standard resolver const GetTodosResolver = RequestResolver.fromEffect( (_: GetTodos): Effect.Effect => Effect.tryPromise({ try: () => fetch("https://api.example.demo/todos").then( (res) => res.json() as Promise> ), catch: () => new GetTodosError() }) ) // Assuming GetUserById can be batched, we create a batched resolver const GetUserByIdResolver = RequestResolver.makeBatched( (requests: ReadonlyArray) => Effect.tryPromise({ try: () => fetch("https://api.example.demo/getUserByIdBatch", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ users: requests.map(({ id }) => ({ id })) }) }).then((res) => res.json()) as Promise>, catch: () => new GetUserError() }).pipe( Effect.andThen((users) => Effect.forEach(requests, (request, index) => Request.completeEffect(request, Effect.succeed(users[index]!)) ) ), Effect.catchAll((error) => Effect.forEach(requests, (request) => Request.completeEffect(request, Effect.fail(error)) ) ) ) ) // Assuming SendEmail can be batched, we create a batched resolver const SendEmailResolver = RequestResolver.makeBatched( (requests: ReadonlyArray) => Effect.tryPromise({ try: () => fetch("https://api.example.demo/sendEmailBatch", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ emails: requests.map(({ address, text }) => ({ address, text })) }) }).then((res) => res.json() as Promise), catch: () => new SendEmailError() }).pipe( Effect.andThen( Effect.forEach(requests, (request) => Request.completeEffect(request, Effect.void) ) ), Effect.catchAll((error) => Effect.forEach(requests, (request) => Request.completeEffect(request, Effect.fail(error)) ) ) ) ) // ------------------------------ // Queries // ------------------------------ // Defines a query to fetch all Todo items const getTodos: Effect.Effect< Array, GetTodosError > = Effect.request(GetTodos({}), GetTodosResolver) // Defines a query to fetch a user by their ID const getUserById = (id: number) => Effect.request(GetUserById({ id }), GetUserByIdResolver) // Defines a query to send an email to a specific address const sendEmail = (address: string, text: string) => Effect.request(SendEmail({ address, text }), SendEmailResolver) // Composes getUserById and sendEmail to send an email to a specific user const sendEmailToUser = (id: number, message: string) => getUserById(id).pipe( Effect.andThen((user) => sendEmail(user.email, message)) ) // Uses getUserById to fetch the owner of a Todo and then sends them an email notification const notifyOwner = (todo: Todo) => getUserById(todo.ownerId).pipe( Effect.andThen((user) => sendEmailToUser(user.id, `hey ${user.name} you got a todo!`) ) ) ``` By using the `Effect.request` function, we integrate the resolvers with the request model effectively. This approach ensures that each query is optimally resolved using the appropriate resolver. Although the code structure looks similar to earlier examples, employing resolvers significantly enhances efficiency by optimizing how requests are handled and reducing unnecessary API calls. ```ts {4} showLineNumbers=false const program = Effect.gen(function* () { const todos = yield* getTodos yield* Effect.forEach(todos, (todo) => notifyOwner(todo), { batching: true }) }) ``` In the final setup, this program will execute only **3** queries to the APIs, regardless of the number of todos. This contrasts sharply with the traditional approach, which would potentially execute **1 + 2n** queries, where **n** is the number of todos. This represents a significant improvement in efficiency, especially for applications with a high volume of data interactions. ### Disabling Batching Batching can be locally disabled using the `Effect.withRequestBatching` utility in the following way: ```ts {6} showLineNumbers=false const program = Effect.gen(function* () { const todos = yield* getTodos yield* Effect.forEach(todos, (todo) => notifyOwner(todo), { concurrency: "unbounded" }) }).pipe(Effect.withRequestBatching(false)) ``` ### Resolvers with Context In complex applications, resolvers often need access to shared services or configurations to handle requests effectively. However, maintaining the ability to batch requests while providing the necessary context can be challenging. Here, we'll explore how to manage context in resolvers to ensure that batching capabilities are not compromised. When creating request resolvers, it's crucial to manage the context carefully. Providing too much context or providing varying services to resolvers can make them incompatible for batching. To prevent such issues, the context for the resolver used in `Effect.request` is explicitly set to `never`. This forces developers to clearly define how the context is accessed and used within resolvers. Consider the following example where we set up an HTTP service that the resolvers can use to execute API calls: ```ts twoslash collapse={7-31,37-65} import { Effect, Context, RequestResolver, Request } from "effect" // ------------------------------ // Model // ------------------------------ interface User { readonly _tag: "User" readonly id: number readonly name: string readonly email: string } class GetUserError { readonly _tag = "GetUserError" } interface Todo { readonly _tag: "Todo" readonly id: number readonly message: string readonly ownerId: number } class GetTodosError { readonly _tag = "GetTodosError" } class SendEmailError { readonly _tag = "SendEmailError" } // ------------------------------ // Requests // ------------------------------ // Define a request to get multiple Todo items which might // fail with a GetTodosError interface GetTodos extends Request.Request, GetTodosError> { readonly _tag: "GetTodos" } // Create a tagged constructor for GetTodos requests const GetTodos = Request.tagged("GetTodos") // Define a request to fetch a User by ID which might // fail with a GetUserError interface GetUserById extends Request.Request { readonly _tag: "GetUserById" readonly id: number } // Create a tagged constructor for GetUserById requests const GetUserById = Request.tagged("GetUserById") // Define a request to send an email which might // fail with a SendEmailError interface SendEmail extends Request.Request { readonly _tag: "SendEmail" readonly address: string readonly text: string } // Create a tagged constructor for SendEmail requests const SendEmail = Request.tagged("SendEmail") // ------------------------------ // Resolvers With Context // ------------------------------ class HttpService extends Context.Tag("HttpService")< HttpService, { fetch: typeof fetch } >() {} const GetTodosResolver = // we create a normal resolver like we did before RequestResolver.fromEffect((_: GetTodos) => Effect.andThen(HttpService, (http) => Effect.tryPromise({ try: () => http .fetch("https://api.example.demo/todos") .then((res) => res.json() as Promise>), catch: () => new GetTodosError() }) ) ).pipe( // we list the tags that the resolver can access RequestResolver.contextFromServices(HttpService) ) ``` We can see now that the type of `GetTodosResolver` is no longer a `RequestResolver` but instead it is: ```ts showLineNumbers=false const GetTodosResolver: Effect< RequestResolver, never, HttpService > ``` which is an effect that access the `HttpService` and returns a composed resolver that has the minimal context ready to use. Once we have such effect we can directly use it in our query definition: ```ts showLineNumbers=false const getTodos: Effect.Effect = Effect.request(GetTodos({}), GetTodosResolver) ``` We can see that the Effect correctly requires `HttpService` to be provided. Alternatively you can create `RequestResolver`s as part of layers direcly accessing or closing over context from construction. **Example** ```ts twoslash collapse={7-31,37-65,71-91} import { Effect, Context, RequestResolver, Request, Layer } from "effect" // ------------------------------ // Model // ------------------------------ interface User { readonly _tag: "User" readonly id: number readonly name: string readonly email: string } class GetUserError { readonly _tag = "GetUserError" } interface Todo { readonly _tag: "Todo" readonly id: number readonly message: string readonly ownerId: number } class GetTodosError { readonly _tag = "GetTodosError" } class SendEmailError { readonly _tag = "SendEmailError" } // ------------------------------ // Requests // ------------------------------ // Define a request to get multiple Todo items which might // fail with a GetTodosError interface GetTodos extends Request.Request, GetTodosError> { readonly _tag: "GetTodos" } // Create a tagged constructor for GetTodos requests const GetTodos = Request.tagged("GetTodos") // Define a request to fetch a User by ID which might // fail with a GetUserError interface GetUserById extends Request.Request { readonly _tag: "GetUserById" readonly id: number } // Create a tagged constructor for GetUserById requests const GetUserById = Request.tagged("GetUserById") // Define a request to send an email which might // fail with a SendEmailError interface SendEmail extends Request.Request { readonly _tag: "SendEmail" readonly address: string readonly text: string } // Create a tagged constructor for SendEmail requests const SendEmail = Request.tagged("SendEmail") // ------------------------------ // Resolvers With Context // ------------------------------ class HttpService extends Context.Tag("HttpService")< HttpService, { fetch: typeof fetch } >() {} const GetTodosResolver = // we create a normal resolver like we did before RequestResolver.fromEffect((_: GetTodos) => Effect.andThen(HttpService, (http) => Effect.tryPromise({ try: () => http .fetch("https://api.example.demo/todos") .then((res) => res.json() as Promise>), catch: () => new GetTodosError() }) ) ).pipe( // we list the tags that the resolver can access RequestResolver.contextFromServices(HttpService) ) // ------------------------------ // Layers // ------------------------------ class TodosService extends Context.Tag("TodosService")< TodosService, { getTodos: Effect.Effect, GetTodosError> } >() {} const TodosServiceLive = Layer.effect( TodosService, Effect.gen(function* () { const http = yield* HttpService const resolver = RequestResolver.fromEffect((_: GetTodos) => Effect.tryPromise({ try: () => http .fetch("https://api.example.demo/todos") .then((res) => res.json()), catch: () => new GetTodosError() }) ) return { getTodos: Effect.request(GetTodos({}), resolver) } }) ) const getTodos: Effect.Effect< Array, GetTodosError, TodosService > = Effect.andThen(TodosService, (service) => service.getTodos) ``` This way is probably the best for most of the cases given that layers are the natural primitive where to wire services together. ## Caching While we have significantly optimized request batching, there's another area that can enhance our application's efficiency: caching. Without caching, even with optimized batch processing, the same requests could be executed multiple times, leading to unnecessary data fetching. In the Effect library, caching is handled through built-in utilities that allow requests to be stored temporarily, preventing the need to re-fetch data that hasn't changed. This feature is crucial for reducing the load on both the server and the network, especially in applications that make frequent similar requests. Here's how you can implement caching for the `getUserById` query: ```ts {3} showLineNumbers=false const getUserById = (id: number) => Effect.request(GetUserById({ id }), GetUserByIdResolver).pipe( Effect.withRequestCaching(true) ) ``` ## Final Program Assuming you've wired everything up correctly: ```ts showLineNumbers=false const program = Effect.gen(function* () { const todos = yield* getTodos yield* Effect.forEach(todos, (todo) => notifyOwner(todo), { concurrency: "unbounded" }) }).pipe(Effect.repeat(Schedule.fixed("10 seconds"))) ``` With this program, the `getTodos` operation retrieves the todos for each user. Then, the `Effect.forEach` function is used to notify the owner of each todo concurrently, without waiting for the notifications to complete. The `repeat` function is applied to the entire chain of operations, and it ensures that the program repeats every 10 seconds using a fixed schedule. This means that the entire process, including fetching todos and sending notifications, will be executed repeatedly with a 10-second interval. The program incorporates a caching mechanism, which prevents the same `GetUserById` operation from being executed more than once within a span of 1 minute. This default caching behavior helps optimize the program's execution and reduces unnecessary requests to fetch user data. Furthermore, the program is designed to send emails in batches, allowing for efficient processing and better utilization of resources. ## Customizing Request Caching In real-world applications, effective caching strategies can significantly improve performance by reducing redundant data fetching. The Effect library provides flexible caching mechanisms that can be tailored for specific parts of your application or applied globally. There may be scenarios where different parts of your application have unique caching requirements—some might benefit from a localized cache, while others might need a global cache setup. Let’s explore how you can configure a custom cache to meet these varied needs. ### Creating a Custom Cache Here's how you can create a custom cache and apply it to part of your application. This example demonstrates setting up a cache that repeats a task every 10 seconds, caching requests with specific parameters like capacity and TTL (time-to-live). ```ts showLineNumbers=false const program = Effect.gen(function* () { const todos = yield* getTodos yield* Effect.forEach(todos, (todo) => notifyOwner(todo), { concurrency: "unbounded" }) }).pipe( Effect.repeat(Schedule.fixed("10 seconds")), Effect.provide( Layer.setRequestCache( Request.makeCache({ capacity: 256, timeToLive: "60 minutes" }) ) ) ) ``` ### Direct Cache Application You can also construct a cache using `Request.makeCache` and apply it directly to a specific program using `Effect.withRequestCache`. This method ensures that all requests originating from the specified program are managed through the custom cache, provided that caching is enabled. # [Configuration](https://effect.website/docs/configuration/) ## Overview import { Aside, Badge } from "@astrojs/starlight/components" Configuration is an essential aspect of any cloud-native application. Effect simplifies the process of managing configuration by offering a convenient interface for configuration providers. The configuration front-end in Effect enables ecosystem libraries and applications to specify their configuration requirements in a declarative manner. It offloads the complex tasks to a `ConfigProvider`, which can be supplied by third-party libraries. Effect comes bundled with a straightforward default `ConfigProvider` that retrieves configuration data from environment variables. This default provider can be used during development or as a starting point before transitioning to more advanced configuration providers. To make our application configurable, we need to understand three essential elements: - **Config Description**: We describe the configuration data using an instance of `Config`. If the configuration data is simple, such as a `string`, `number`, or `boolean`, we can use the built-in functions provided by the `Config` module. For more complex data types like [HostPort](#custom-configuration-types), we can combine primitive configs to create a custom configuration description. - **Config Frontend**: We utilize the instance of `Config` to load the configuration data described by the instance (a `Config` is, in itself, an effect). This process leverages the current `ConfigProvider` to retrieve the configuration. - **Config Backend**: The `ConfigProvider` serves as the underlying engine that manages the configuration loading process. Effect comes with a default config provider as part of its default services. This default provider reads the configuration data from environment variables. If we want to use a custom config provider, we can utilize the `Effect.withConfigProvider` API to configure the Effect runtime accordingly. ## Basic Configuration Types Effect provides several built-in types for configuration values, which you can use right out of the box: | Type | Description | | ---------- | ----------------------------------------------------------------------- | | `string` | Reads a configuration value as a string. | | `number` | Reads a value as a floating-point number. | | `boolean` | Reads a value as a boolean (`true` or `false`). | | `integer` | Reads a value as an integer. | | `date` | Parses a value into a `Date` object. | | `literal` | Reads a fixed literal (\*). | | `logLevel` | Reads a value as a [LogLevel](/docs/observability/logging/#log-levels). | | `duration` | Parses a value as a time duration. | | `redacted` | Reads a **sensitive value**, ensuring it is protected when logged. | | `url` | Parses a value as a valid URL. | (\*) `string | number | boolean | null | bigint` **Example** (Loading Environment Variables) Here's an example of loading a basic configuration using environment variables for `HOST` and `PORT`: ```ts twoslash title="primitives.ts" import { Effect, Config } from "effect" // Define a program that loads HOST and PORT configuration const program = Effect.gen(function* () { const host = yield* Config.string("HOST") // Read as a string const port = yield* Config.number("PORT") // Read as a number console.log(`Application started: ${host}:${port}`) }) Effect.runPromise(program) ``` If you run this without setting the required environment variables: ```sh showLineNumbers=false npx tsx primitives.ts ``` you'll see an error indicating the missing configuration: ```ansi showLineNumbers=false [Error: (Missing data at HOST: "Expected HOST to exist in the process context")] { name: '(FiberFailure) Error', [Symbol(effect/Runtime/FiberFailure)]: Symbol(effect/Runtime/FiberFailure), [Symbol(effect/Runtime/FiberFailure/Cause)]: { _tag: 'Fail', error: { _op: 'MissingData', path: [ 'HOST' ], message: 'Expected HOST to exist in the process context' } } } ``` To run the program successfully, set the environment variables as shown below: ```sh showLineNumbers=false HOST=localhost PORT=8080 npx tsx primitives.ts ``` Output: ```ansi showLineNumbers=false Application started: localhost:8080 ``` ## Using Config with Schema You can define and decode configuration values using a schema. **Example** (Decoding a Configuration Value) ```ts twoslash import { Effect, Schema } from "effect" // Define a config that expects a string with at least 4 characters const myConfig = Schema.Config( "Foo", Schema.String.pipe(Schema.minLength(4)) ) ``` For more information, see the [Schema.Config](/docs/schema/effect-data-types/#config) documentation. ## Providing Default Values Sometimes, you may encounter situations where an environment variable is missing, leading to an incomplete configuration. To address this, Effect provides the `Config.withDefault` function, which allows you to specify a default value. This fallback ensures that your application continues to function even if a required environment variable is not set. **Example** (Using Default Values) ```ts twoslash title="defaults.ts" import { Effect, Config } from "effect" const program = Effect.gen(function* () { const host = yield* Config.string("HOST") // Use default 8080 if PORT is not set const port = yield* Config.number("PORT").pipe(Config.withDefault(8080)) console.log(`Application started: ${host}:${port}`) }) Effect.runPromise(program) ``` Running this program with only the `HOST` environment variable set: ```sh showLineNumbers=false HOST=localhost npx tsx defaults.ts ``` produces the following output: ```ansi showLineNumbers=false Application started: localhost:8080 ``` In this case, even though the `PORT` environment variable is not set, the program continues to run, using the default value of `8080` for the port. This ensures that the application remains functional without requiring every configuration to be explicitly provided. ## Handling Sensitive Values Some configuration values, like API keys, should not be printed in logs. The `Config.redacted` function is used to handle sensitive information safely. It parses the configuration value and wraps it in a `Redacted`, a specialized [data type](/docs/data-types/redacted/) designed to protect secrets. When you log a `Redacted` value using `console.log`, the actual content remains hidden, providing an extra layer of security. To access the real value, you must explicitly use `Redacted.value`. **Example** (Protecting Sensitive Data) ```ts twoslash title="redacted.ts" import { Effect, Config, Redacted } from "effect" const program = Effect.gen(function* () { // ┌─── Redacted // ▼ const redacted = yield* Config.redacted("API_KEY") // Log the redacted value, which won't reveal the actual secret console.log(`Console output: ${redacted}`) // Access the real value using Redacted.value and log it console.log(`Actual value: ${Redacted.value(redacted)}`) }) Effect.runPromise(program) ``` When this program is executed: ```sh showLineNumbers=false API_KEY=my-api-key tsx redacted.ts ``` The output will look like this: ```ansi showLineNumbers=false Console output: Actual value: my-api-key ``` As shown, when logging the `Redacted` value using `console.log`, the output is ``, ensuring that sensitive data remains concealed. However, by using `Redacted.value`, the true value (`"my-api-key"`) can be accessed and displayed, providing controlled access to the secret. ### Wrapping a Config with Redacted By default, when you pass a string to `Config.redacted`, it returns a `Redacted`. You can also pass a `Config` (such as `Config.number`) to ensure that only validated values are accepted. This adds an extra layer of security by ensuring that sensitive data is properly validated before being redacted. **Example** (Redacting and Validating a Number) ```ts twoslash import { Effect, Config, Redacted } from "effect" const program = Effect.gen(function* () { // Wrap the validated number configuration with redaction // // ┌─── Redacted // ▼ const redacted = yield* Config.redacted(Config.number("SECRET")) console.log(`Console output: ${redacted}`) console.log(`Actual value: ${Redacted.value(redacted)}`) }) Effect.runPromise(program) ``` ## Combining Configurations Effect provides several built-in combinators that allow you to define and manipulate configurations. These combinators take a `Config` as input and produce another `Config`, enabling more complex configuration structures. | Combinator | Description | | ---------- | ------------------------------------------------------------------------------------------------------------------- | | `array` | Constructs a configuration for an array of values. | | `chunk` | Constructs a configuration for a sequence of values. | | `option` | Returns an optional configuration. If the data is missing, the result will be `None`; otherwise, it will be `Some`. | | `repeat` | Describes a sequence of values, each following the structure of the given config. | | `hashSet` | Constructs a configuration for a set of values. | | `hashMap` | Constructs a configuration for a key-value map. | Additionally, there are three special combinators for specific use cases: | Combinator | Description | | ---------- | ------------------------------------------------------------------------ | | `succeed` | Constructs a config that contains a predefined value. | | `fail` | Constructs a config that fails with the specified error message. | | `all` | Combines multiple configurations into a tuple, struct, or argument list. | **Example** (Using the `array` combinator) The following example demonstrates how to load an environment variable as an array of strings using the `Config.array` constructor. ```ts twoslash title="index.ts" import { Config, Effect } from "effect" const program = Effect.gen(function* () { const config = yield* Config.array(Config.string(), "MYARRAY") console.log(config) }) Effect.runPromise(program) // Run: // MYARRAY=a,b,c,a npx tsx index.ts // Output: // [ 'a', 'b c', 'd', 'a' ] ``` **Example** (Using the `hashSet` combinator) ```ts twoslash title="index.ts" import { Config, Effect } from "effect" const program = Effect.gen(function* () { const config = yield* Config.hashSet(Config.string(), "MYSET") console.log(config) }) Effect.runPromise(program) // Run: // MYSET=a,"b c",d,a npx tsx index.ts // Output: // { _id: 'HashSet', values: [ 'd', 'a', 'b c' ] } ``` **Example** (Using the `hashMap` combinator) ```ts twoslash title="index.ts" import { Config, Effect } from "effect" const program = Effect.gen(function* () { const config = yield* Config.hashMap(Config.string(), "MYMAP") console.log(config) }) Effect.runPromise(program) // Run: // MYMAP_A=a MYMAP_B=b npx tsx index.ts // Output: // { _id: 'HashMap', values: [ [ 'A', 'a' ], [ 'B', 'b' ] ] } ``` ## Operators Effect provides several built-in operators to work with configurations, allowing you to manipulate and transform them according to your needs. ### Transforming Operators These operators enable you to modify configurations or validate their values: | Operator | Description | | ------------ | --------------------------------------------------------------------------------------------------------- | | `validate` | Ensures that a configuration meets certain criteria, returning a validation error if it does not. | | `map` | Transforms the values of a configuration using a provided function. | | `mapAttempt` | Similar to `map`, but catches any errors thrown by the function and converts them into validation errors. | | `mapOrFail` | Like `map`, but the function can fail. If it does, the result is a validation error. | **Example** (Using `validate` Operator) ```ts twoslash title="validate.ts" import { Effect, Config } from "effect" const program = Effect.gen(function* () { // Load the NAME environment variable and validate its length const config = yield* Config.string("NAME").pipe( Config.validate({ message: "Expected a string at least 4 characters long", validation: (s) => s.length >= 4 }) ) console.log(config) }) Effect.runPromise(program) ``` If we run this program with an invalid `NAME` value: ```sh showLineNumbers=false NAME=foo npx tsx validate.ts ``` The output will be: ```ansi showLineNumbers=false [Error: (Invalid data at NAME: "Expected a string at least 4 characters long")] { name: '(FiberFailure) Error', [Symbol(effect/Runtime/FiberFailure)]: Symbol(effect/Runtime/FiberFailure), [Symbol(effect/Runtime/FiberFailure/Cause)]: { _tag: 'Fail', error: { _op: 'InvalidData', path: [ 'NAME' ], message: 'Expected a string at least 4 characters long' } } } ``` ### Fallback Operators Fallback operators are useful when you want to provide alternative configurations in case of errors or missing data. These operators ensure that your program can still run even if some configuration values are unavailable. | Operator | Description | | ---------- | ----------------------------------------------------------------------------------------------------- | | `orElse` | Attempts to use the primary config first. If it fails or is missing, it falls back to another config. | | `orElseIf` | Similar to `orElse`, but it switches to the fallback config only if the error matches a condition. | **Example** (Using `orElse` for Fallback) In this example, the program requires two configuration values: `A` and `B`. We set up two configuration providers, each containing only one of the required values. Using the `orElse` operator, we combine these providers so the program can retrieve both `A` and `B`. ```ts twoslash title="orElse.ts" import { Config, ConfigProvider, Effect } from "effect" // A program that requires two configurations: A and B const program = Effect.gen(function* () { const A = yield* Config.string("A") // Retrieve config A const B = yield* Config.string("B") // Retrieve config B console.log(`A: ${A}, B: ${B}`) }) // First provider has A but is missing B const provider1 = ConfigProvider.fromMap(new Map([["A", "A"]])) // Second provider has B but is missing A const provider2 = ConfigProvider.fromMap(new Map([["B", "B"]])) // Use `orElse` to fall back from provider1 to provider2 const provider = provider1.pipe(ConfigProvider.orElse(() => provider2)) Effect.runPromise(Effect.withConfigProvider(program, provider)) ``` If we run this program: ```sh showLineNumbers=false npx tsx orElse.ts ``` The output will be: ```ansi showLineNumbers=false A: A, B: B ``` ## Custom Configuration Types Effect allows you to define configurations for custom types by combining primitive configurations using [combinators](#combining-configurations) and [operators](#operators). For example, let's create a `HostPort` class, which has two fields: `host` and `port`. ```ts twoslash class HostPort { constructor(readonly host: string, readonly port: number) {} get url() { return `${this.host}:${this.port}` } } ``` To define a configuration for this custom type, we can combine primitive configs for `string` and `number`: **Example** (Defining a Custom Configuration) ```ts twoslash import { Config } from "effect" class HostPort { constructor(readonly host: string, readonly port: number) {} get url() { return `${this.host}:${this.port}` } } // Combine the configuration for 'HOST' and 'PORT' const both = Config.all([Config.string("HOST"), Config.number("PORT")]) // Map the configuration values into a HostPort instance const config = Config.map( both, ([host, port]) => new HostPort(host, port) ) ``` In this example, `Config.all(configs)` combines two primitive configurations, `Config` and `Config`, into a `Config<[string, number]>`. The `Config.map` operator is then used to transform these values into an instance of the `HostPort` class. **Example** (Using Custom Configuration) ```ts twoslash title="App.ts" import { Effect, Config } from "effect" class HostPort { constructor(readonly host: string, readonly port: number) {} get url() { return `${this.host}:${this.port}` } } // Combine the configuration for 'HOST' and 'PORT' const both = Config.all([Config.string("HOST"), Config.number("PORT")]) // Map the configuration values into a HostPort instance const config = Config.map( both, ([host, port]) => new HostPort(host, port) ) // Main program that reads configuration and starts the application const program = Effect.gen(function* () { const hostPort = yield* config console.log(`Application started: ${hostPort.url}`) }) Effect.runPromise(program) ``` When you run this program, it will try to retrieve the values for `HOST` and `PORT` from your environment variables: ```sh showLineNumbers=false HOST=localhost PORT=8080 npx tsx App.ts ``` If successful, it will print: ```ansi showLineNumbers=false Application started: localhost:8080 ``` ## Nested Configurations We've seen how to define configurations at the top level, whether for primitive or custom types. In some cases, though, you might want to structure your configurations in a more nested way, organizing them under common namespaces for clarity and manageability. For instance, consider the following `ServiceConfig` type: ```ts twoslash class ServiceConfig { constructor( readonly host: string, readonly port: number, readonly timeout: number ) {} get url() { return `${this.host}:${this.port}` } } ``` If you were to use this configuration in your application, it would expect the `HOST`, `PORT`, and `TIMEOUT` environment variables at the top level. But in many cases, you may want to organize configurations under a shared namespace—for example, grouping `HOST` and `PORT` under a `SERVER` namespace, while keeping `TIMEOUT` at the root. To do this, you can use the `Config.nested` operator, which allows you to nest configuration values under a specific namespace. Let's update the previous example to reflect this: ```ts twoslash import { Config } from "effect" class ServiceConfig { constructor( readonly host: string, readonly port: number, readonly timeout: number ) {} get url() { return `${this.host}:${this.port}` } } const serverConfig = Config.all([ Config.string("HOST"), Config.number("PORT") ]) const serviceConfig = Config.map( Config.all([ // Read 'HOST' and 'PORT' from 'SERVER' namespace Config.nested(serverConfig, "SERVER"), // Read 'TIMEOUT' from the root namespace Config.number("TIMEOUT") ]), ([[host, port], timeout]) => new ServiceConfig(host, port, timeout) ) ``` Now, if you run your application with this configuration setup, it will look for the following environment variables: - `SERVER_HOST` for the host value - `SERVER_PORT` for the port value - `TIMEOUT` for the timeout value This structured approach keeps your configuration more organized, especially when dealing with multiple services or complex applications. ## Mocking Configurations in Tests When testing services, there are times when you need to provide specific configurations for your tests. To simulate this, it's useful to mock the configuration backend that reads these values. You can achieve this using the `ConfigProvider.fromMap` constructor. This method allows you to create a configuration provider from a `Map`, where the map represents the configuration data. You can then use this mock provider in place of the default one by calling `Effect.withConfigProvider`. **Example** (Mocking a Config Provider for Testing) ```ts twoslash import { Config, ConfigProvider, Effect } from "effect" class HostPort { constructor(readonly host: string, readonly port: number) {} get url() { return `${this.host}:${this.port}` } } const config = Config.map( Config.all([Config.string("HOST"), Config.number("PORT")]), ([host, port]) => new HostPort(host, port) ) const program = Effect.gen(function* () { const hostPort = yield* config console.log(`Application started: ${hostPort.url}`) }) // Create a mock config provider using a map with test data const mockConfigProvider = ConfigProvider.fromMap( new Map([ ["HOST", "localhost"], ["PORT", "8080"] ]) ) // Run the program using the mock config provider Effect.runPromise(Effect.withConfigProvider(program, mockConfigProvider)) // Output: Application started: localhost:8080 ``` This approach helps you create isolated tests that don't rely on external environment variables, ensuring your tests run consistently with mock configurations. ### Handling Nested Configuration Values For more complex setups, configurations often include nested keys. By default, `ConfigProvider.fromMap` uses `.` as the separator for nested keys. **Example** (Providing Nested Configuration Values) ```ts twoslash import { Config, ConfigProvider, Effect } from "effect" const config = Config.nested(Config.number("PORT"), "SERVER") const program = Effect.gen(function* () { const port = yield* config console.log(`Server is running on port ${port}`) }) // Mock configuration using '.' as the separator for nested keys const mockConfigProvider = ConfigProvider.fromMap( new Map([["SERVER.PORT", "8080"]]) ) Effect.runPromise(Effect.withConfigProvider(program, mockConfigProvider)) // Output: Server is running on port 8080 ``` ### Customizing the Path Delimiter If your configuration data uses a different separator (such as `_`), you can change the delimiter using the `pathDelim` option in `ConfigProvider.fromMap`. **Example** (Using a Custom Path Delimiter) ```ts twoslash import { Config, ConfigProvider, Effect } from "effect" const config = Config.nested(Config.number("PORT"), "SERVER") const program = Effect.gen(function* () { const port = yield* config console.log(`Server is running on port ${port}`) }) // Mock configuration using '_' as the separator const mockConfigProvider = ConfigProvider.fromMap( new Map([["SERVER_PORT", "8080"]]), { pathDelim: "_" } ) Effect.runPromise(Effect.withConfigProvider(program, mockConfigProvider)) // Output: Server is running on port 8080 ``` ## ConfigProvider The `ConfigProvider` module in Effect allows applications to load configuration values from different sources. The default provider reads from environment variables, but you can customize its behavior when needed. ### Loading Configuration from Environment Variables The `ConfigProvider.fromEnv` function creates a `ConfigProvider` that loads values from environment variables. This is the default provider used by Effect unless another is specified. If your application requires a custom delimiter for nested configuration keys, you can configure `ConfigProvider.fromEnv` accordingly. **Example** (Changing the Path Delimiter) The following example modifies the path delimiter (`"__"`) and sequence delimiter (`"|"`) for environment variables. ```ts twoslash title="index.ts" import { Config, ConfigProvider, Effect } from "effect" const program = Effect.gen(function* () { // Read SERVER_HOST and SERVER_PORT as nested configuration values const port = yield* Config.nested(Config.number("PORT"), "SERVER") const host = yield* Config.nested(Config.string("HOST"), "SERVER") console.log(`Application started: ${host}:${port}`) }) Effect.runPromise( Effect.withConfigProvider( program, // Custom delimiters ConfigProvider.fromEnv({ pathDelim: "__", seqDelim: "|" }) ) ) ``` To match the custom delimiter (`"__"`), set environment variables like this: ```sh showLineNumbers=false SERVER__HOST=localhost SERVER__PORT=8080 npx tsx index.ts ``` Output: ```ansi showLineNumbers=false Application started: localhost:8080 ``` ### Loading Configuration from JSON The `ConfigProvider.fromJson` function creates a `ConfigProvider` that loads values from a JSON object. **Example** (Reading Nested Configuration from JSON) ```ts twoslash import { Config, ConfigProvider, Effect } from "effect" const program = Effect.gen(function* () { // Read SERVER_HOST and SERVER_PORT as nested configuration values const port = yield* Config.nested(Config.number("PORT"), "SERVER") const host = yield* Config.nested(Config.string("HOST"), "SERVER") console.log(`Application started: ${host}:${port}`) }) Effect.runPromise( Effect.withConfigProvider( program, ConfigProvider.fromJson( JSON.parse(`{"SERVER":{"PORT":8080,"HOST":"localhost"}}`) ) ) ) // Output: Application started: localhost:8080 ``` ### Using Nested Configuration Namespaces The `ConfigProvider.nested` function allows **grouping configuration values** under a namespace. This is helpful when structuring settings logically, such as grouping `SERVER`-related values. **Example** (Using a Nested Namespace) ```ts twoslash title="index.ts" import { Config, ConfigProvider, Effect } from "effect" const program = Effect.gen(function* () { const port = yield* Config.number("PORT") // Reads SERVER_PORT const host = yield* Config.string("HOST") // Reads SERVER_HOST console.log(`Application started: ${host}:${port}`) }) Effect.runPromise( Effect.withConfigProvider( program, ConfigProvider.fromEnv().pipe( // Uses SERVER as a namespace ConfigProvider.nested("SERVER") ) ) ) ``` Since we defined `"SERVER"` as the namespace, the environment variables must follow this pattern: ```sh showLineNumbers=false SERVER_HOST=localhost SERVER_PORT=8080 npx tsx index.ts ``` Output: ```ansi showLineNumbers=false Application started: localhost:8080 ``` ### Converting Configuration Keys to Constant Case The `ConfigProvider.constantCase` function transforms all configuration keys into constant case (uppercase with underscores). This is useful when adapting environment variables to match different naming conventions. **Example** (Using `constantCase` for Environment Variables) ```ts twoslash title="index.ts" import { Config, ConfigProvider, Effect } from "effect" const program = Effect.gen(function* () { const port = yield* Config.number("Port") // Reads PORT const host = yield* Config.string("Host") // Reads HOST console.log(`Application started: ${host}:${port}`) }) Effect.runPromise( Effect.withConfigProvider( program, // Convert keys to constant case ConfigProvider.fromEnv().pipe(ConfigProvider.constantCase) ) ) ``` Since `constantCase` converts `"Port"` → `"PORT"` and `"Host"` → `"HOST"`, the environment variables must be set as follows: ```sh showLineNumbers=false HOST=localhost PORT=8080 npx tsx index.ts ``` Output: ```ansi showLineNumbers=false Application started: localhost:8080 ``` ## Deprecations ### Secret _Deprecated since version 3.3.0: Please use [Config.redacted](#handling-sensitive-values) for handling sensitive information going forward._ The `Config.secret` function was previously used to secure sensitive information in a similar way to `Config.redacted`. It wraps configuration values in a `Secret` type, which also conceals details when logged but allows access via `Secret.value`. **Example** (Using Deprecated `Config.secret`) ```ts twoslash title="secret.ts" import { Effect, Config, Secret } from "effect" const program = Effect.gen(function* () { const secret = yield* Config.secret("API_KEY") // Log the secret value, which won't reveal the actual secret console.log(`Console output: ${secret}`) // Access the real value using Secret.value and log it console.log(`Actual value: ${Secret.value(secret)}`) }) Effect.runPromise(program) ``` When this program is executed: ```sh showLineNumbers=false API_KEY=my-api-key tsx secret.ts ``` The output will look like this: ```ansi showLineNumbers=false Console output: Secret() Actual value: my-api-key ``` # [Introduction to Runtime](https://effect.website/docs/runtime/) ## Overview The `Runtime` data type represents a runtime system that can **execute effects**. To run an effect, `Effect`, we need a `Runtime` that contains the required resources, denoted by the `R` type parameter. A `Runtime` consists of three main components: - A value of type `Context` - A value of type `FiberRefs` - A value of type `RuntimeFlags` ## What is a Runtime System? When we write an Effect program, we construct an `Effect` using constructors and combinators. Essentially, we are creating a blueprint of a program. An `Effect` is merely a data structure that describes the execution of a concurrent program. It represents a tree-like structure that combines various primitives to define what the effect should do. However, this data structure itself does not perform any actions, it is solely a description of a concurrent program. To execute this program, the Effect runtime system comes into play. The `Runtime.run*` functions (e.g., `Runtime.runPromise`, `Runtime.runFork`) are responsible for taking this blueprint and executing it. When the runtime system runs an effect, it creates a root fiber, initializing it with: - The initial [context](/docs/requirements-management/services/#how-it-works) - The initial `FiberRefs` - The initial effect It then starts a loop, executing the instructions described by the `Effect` step by step. You can think of the runtime as a system that takes an [`Effect`](/docs/getting-started/the-effect-type/) and its associated context `Context` and produces an [`Exit`](/docs/data-types/exit/) result. ```text showLineNumbers=false ┌────────────────────────────────┐ │ Context + Effect │ └────────────────────────────────┘ │ ▼ ┌────────────────────────────────┐ │ Effect Runtime System │ └────────────────────────────────┘ │ ▼ ┌────────────────────────────────┐ │ Exit │ └────────────────────────────────┘ ``` Runtime Systems have a lot of responsibilities: | Responsibility | Description | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------ | | **Executing the program** | The runtime must execute every step of the effect in a loop until the program completes. | | **Handling errors** | It handles both expected and unexpected errors that occur during execution. | | **Managing concurrency** | The runtime spawns new fibers when `Effect.fork` is called to handle concurrent operations. | | **Cooperative yielding** | It ensures fibers don't monopolize resources, yielding control when necessary. | | **Ensuring resource cleanup** | The runtime guarantees finalizers run properly to clean up resources when needed. | | **Handling async callbacks** | The runtime deals with asynchronous operations transparently, allowing you to write async and sync code uniformly. | ## The Default Runtime When we use [functions that run effects](/docs/getting-started/running-effects/) like `Effect.runPromise` or `Effect.runFork`, we are actually using the **default runtime** without explicitly mentioning it. These functions are designed as convenient shortcuts for executing our effects using the default runtime. Each of the `Effect.run*` functions internally calls the corresponding `Runtime.run*` function, passing in the default runtime. For example, `Effect.runPromise` is just an alias for `Runtime.runPromise(defaultRuntime)`. Both of the following executions are functionally equivalent: **Example** (Running an Effect Using the Default Runtime) ```ts twoslash import { Effect, Runtime } from "effect" const program = Effect.log("Application started!") Effect.runPromise(program) /* Output: timestamp=... level=INFO fiber=#0 message="Application started!" */ Runtime.runPromise(Runtime.defaultRuntime)(program) /* Output: timestamp=... level=INFO fiber=#0 message="Application started!" */ ``` In both cases, the program runs using the default runtime, producing the same output. The default runtime includes: - An empty [context](/docs/requirements-management/services/#how-it-works) - A set of `FiberRefs` that include the [default services](/docs/requirements-management/default-services/) - A default configuration for `RuntimeFlags` that enables `Interruption` and `CooperativeYielding` In most scenarios, using the default runtime is sufficient for effect execution. However, there are cases where it's helpful to create a custom runtime, particularly when you need to reuse specific configurations or contexts. For example, in a React app or when executing operations on a server in response to API requests, you might create a `Runtime` by initializing a [layer](/docs/requirements-management/layers/) `Layer`. This allows you to maintain a consistent context across different execution boundaries. ## Locally Scoped Runtime Configuration In Effect, runtime configurations are typically **inherited** from their parent workflows. This means that when we access a runtime configuration or obtain a runtime inside a workflow, we are essentially using the configuration of the parent workflow. However, there are cases where we want to temporarily **override the runtime configuration for a specific part** of our code. This concept is known as locally scoped runtime configuration. Once the execution of that code region is completed, the runtime configuration **reverts** to its original settings. To achieve this, we make use of the `Effect.provide` function, which allow us to provide a new runtime configuration to a specific section of our code. **Example** (Overriding the Logger Configuration) In this example, we create a simple logger using `Logger.replace`, which replaces the default logger with a custom one that logs messages without timestamps or levels. We then use `Effect.provide` to apply this custom logger to the program. ```ts twoslash import { Logger, Effect } from "effect" const addSimpleLogger = Logger.replace( Logger.defaultLogger, // Custom logger implementation Logger.make(({ message }) => console.log(message)) ) const program = Effect.gen(function* () { yield* Effect.log("Application started!") yield* Effect.log("Application is about to exit!") }) // Running with the default logger Effect.runFork(program) /* Output: timestamp=... level=INFO fiber=#0 message="Application started!" timestamp=... level=INFO fiber=#0 message="Application is about to exit!" */ // Overriding the default logger with a custom one Effect.runFork(program.pipe(Effect.provide(addSimpleLogger))) /* Output: [ 'Application started!' ] [ 'Application is about to exit!' ] */ ``` To ensure that the runtime configuration is only applied to a specific part of an Effect application, we should provide the configuration layer exclusively to that particular section. **Example** (Providing a configuration layer to a nested workflow) In this example, we demonstrate how to apply a custom logger configuration only to a specific section of the program. The default logger is used for most of the program, but when we apply the `Effect.provide(addSimpleLogger)` call, it overrides the logger within that specific nested block. After that, the configuration reverts to its original state. ```ts twoslash import { Logger, Effect } from "effect" const addSimpleLogger = Logger.replace( Logger.defaultLogger, // Custom logger implementation Logger.make(({ message }) => console.log(message)) ) const removeDefaultLogger = Logger.remove(Logger.defaultLogger) const program = Effect.gen(function* () { // Logs with default logger yield* Effect.log("Application started!") yield* Effect.gen(function* () { // This log is suppressed yield* Effect.log("I'm not going to be logged!") // Custom logger applied here yield* Effect.log("I will be logged by the simple logger.").pipe( Effect.provide(addSimpleLogger) ) // This log is suppressed yield* Effect.log( "Reset back to the previous configuration, so I won't be logged." ) }).pipe( // Remove the default logger temporarily Effect.provide(removeDefaultLogger) ) // Logs with default logger again yield* Effect.log("Application is about to exit!") }) Effect.runSync(program) /* Output: timestamp=... level=INFO fiber=#0 message="Application started!" [ 'I will be logged by the simple logger.' ] timestamp=... level=INFO fiber=#0 message="Application is about to exit!" */ ``` ## ManagedRuntime When developing an Effect application and using `Effect.run*` functions to execute it, the application is automatically run using the default runtime behind the scenes. While it’s possible to adjust specific parts of the application by providing locally scoped configuration layers using `Effect.provide`, there are scenarios where you might want to **customize the runtime configuration for the entire application** from the top level. In these cases, you can create a top-level runtime by converting a configuration layer into a runtime using the `ManagedRuntime.make` constructor. **Example** (Creating and Using a Custom Managed Runtime) In this example, we first create a custom configuration layer called `appLayer`, which replaces the default logger with a simple one that logs messages to the console. Next, we use `ManagedRuntime.make` to turn this configuration layer into a runtime. ```ts twoslash import { Effect, ManagedRuntime, Logger } from "effect" // Define a configuration layer that replaces the default logger const appLayer = Logger.replace( Logger.defaultLogger, // Custom logger implementation Logger.make(({ message }) => console.log(message)) ) // Create a custom runtime from the configuration layer const runtime = ManagedRuntime.make(appLayer) const program = Effect.log("Application started!") // Execute the program using the custom runtime runtime.runSync(program) // Clean up resources associated with the custom runtime Effect.runFork(runtime.disposeEffect) /* Output: [ 'Application started!' ] */ ``` ### Effect.Tag When working with runtimes that you pass around, `Effect.Tag` can help simplify the access to services. It lets you define a new tag and embed the service shape directly into the static properties of the tag class. **Example** (Defining a Tag for Notifications) ```ts twoslash import { Effect } from "effect" class Notifications extends Effect.Tag("Notifications")< Notifications, { readonly notify: (message: string) => Effect.Effect } >() {} ``` In this setup, the fields of the service (in this case, the `notify` method) are turned into static properties of the `Notifications` class, making it easier to access them. This allows you to interact with the service directly: **Example** (Using the Notifications Tag) ```ts twoslash import { Effect } from "effect" class Notifications extends Effect.Tag("Notifications")< Notifications, { readonly notify: (message: string) => Effect.Effect } >() {} // Create an effect that depends on the Notifications service const action = Notifications.notify("Hello, world!") // ^? const action: Effect ``` In this example, the `action` effect depends on the `Notifications` service. This approach allows you to reference services without manually passing them around. Later, you can create a `Layer` that provides the `Notifications` service and build a `ManagedRuntime` with that layer to ensure the service is available where needed. ### Integrations The `ManagedRuntime` simplifies the integration of services and layers with other frameworks or tools, particularly in environments where Effect is not the primary framework and access to the main entry point is restricted. For example, in environments like React or other frameworks where you have limited control over the main application entry point, `ManagedRuntime` helps manage the lifecycle of services. Here's how to manage a service's lifecycle within an external framework: **Example** (Using `ManagedRuntime` in an External Framework) ```ts twoslash import { Effect, ManagedRuntime, Layer, Console } from "effect" // Define the Notifications service using Effect.Tag class Notifications extends Effect.Tag("Notifications")< Notifications, { readonly notify: (message: string) => Effect.Effect } >() { // Provide a live implementation of the Notifications service static Live = Layer.succeed(this, { notify: (message) => Console.log(message) }) } // Example entry point for an external framework async function main() { // Create a custom runtime using the Notifications layer const runtime = ManagedRuntime.make(Notifications.Live) // Run the effect await runtime.runPromise(Notifications.notify("Hello, world!")) // Dispose of the runtime, cleaning up resources await runtime.dispose() } ``` # [API Reference](https://effect.website/docs/additional-resources/api-reference/) ## Overview - [`effect`](https://effect-ts.github.io/effect/docs/effect) - [`@effect/cli`](https://effect-ts.github.io/effect/docs/cli) ([Getting Started](https://github.com/Effect-TS/effect/blob/main/packages/cli/README.md)) - [`@effect/opentelemetry`](https://effect-ts.github.io/effect/docs/opentelemetry) - [`@effect/platform`](https://effect-ts.github.io/effect/docs/platform) ([Experimental Features](https://github.com/Effect-TS/effect/blob/main/packages/platform/README.md)) - [`@effect/printer`](https://effect-ts.github.io/effect/docs/printer) ([Getting Started](https://github.com/Effect-TS/effect/blob/main/packages/printer/README.md)) - [`@effect/rpc`](https://effect-ts.github.io/effect/docs/rpc) ([Getting Started](https://github.com/Effect-TS/effect/blob/main/packages/rpc/README.md)) - [`@effect/typeclass`](https://effect-ts.github.io/effect/docs/typeclass) ([Getting Started](https://github.com/Effect-TS/effect/blob/main/packages/typeclass/README.md)) # [Coming From ZIO](https://effect.website/docs/additional-resources/coming-from-zio/) ## Overview If you are coming to Effect from ZIO, there are a few differences to be aware of. ## Environment In Effect, we represent the environment required to run an effect workflow as a **union** of services: **Example** (Defining the Environment with a Union of Services) ```ts twoslash "Console | Logger" import { Effect } from "effect" interface IOError { readonly _tag: "IOError" } interface HttpError { readonly _tag: "HttpError" } interface Console { readonly log: (msg: string) => void } interface Logger { readonly log: (msg: string) => void } type Response = Record // `R` is a union of `Console` and `Logger` type Http = Effect.Effect ``` This may be confusing to folks coming from ZIO, where the environment is represented as an **intersection** of services: ```scala showLineNumbers=false type Http = ZIO[Console with Logger, IOError, Response] ``` ## Rationale The rationale for using a union to represent the environment required by an `Effect` workflow boils down to our desire to remove `Has` as a wrapper for services in the environment (similar to what was achieved in ZIO 2.0). To be able to remove `Has` from Effect, we had to think a bit more structurally given that TypeScript is a structural type system. In TypeScript, if you have a type `A & B` where there is a structural conflict between `A` and `B`, the type `A & B` will reduce to `never`. **Example** (Intersection Type Conflict) ```ts twoslash interface A { readonly prop: string } interface B { readonly prop: number } const ab: A & B = { // @ts-expect-error prop: "" } /* Type 'string' is not assignable to type 'never'.ts(2322) */ ``` In previous versions of Effect, intersections were used for representing an environment with multiple services. The problem with using intersections (i.e. `A & B`) is that there could be multiple services in the environment that have functions and properties named in the same way. To remedy this, we wrapped services in the `Has` type (similar to ZIO 1.0), so you would have `Has & Has` in your environment. In ZIO 2.0, the _contravariant_ `R` type parameter of the `ZIO` type (representing the environment) became fully phantom, thus allowing for removal of the `Has` type. This significantly improved the clarity of type signatures as well as removing another "stumbling block" for new users. To facilitate removal of `Has` in Effect, we had to consider how types in the environment compose. By the rule of composition, contravariant parameters composed as an intersection (i.e. with `&`) are equivalent to covariant parameters composed together as a union (i.e. with `|`) for purposes of assignability. Based upon this fact, we decided to diverge from ZIO and make the `R` type parameter _covariant_ given `A | B` does not reduce to `never` if `A` and `B` have conflicts. From our example above: ```ts twoslash interface A { readonly prop: string } interface B { readonly prop: number } // ok const ab: A | B = { prop: "" } ``` Representing `R` as a covariant type parameter containing the union of services required by an `Effect` workflow allowed us to remove the requirement for `Has`. ## Type Aliases In Effect, there are no predefined type aliases such as `UIO`, `URIO`, `RIO`, `Task`, or `IO` like in ZIO. The reason for this is that type aliases are lost as soon as you compose them, which renders them somewhat useless unless you maintain **multiple** signatures for **every** function. In Effect, we have chosen not to go down this path. Instead, we utilize the `never` type to indicate unused types. It's worth mentioning that the perception of type aliases being quicker to understand is often just an illusion. In Effect, the explicit notation `Effect` clearly communicates that only type `A` is being used. On the other hand, when using a type alias like `RIO`, questions arise about the type `E`. Is it `unknown`? `never`? Remembering such details becomes challenging. # [Effect vs fp-ts](https://effect.website/docs/additional-resources/effect-vs-fp-ts/) ## Overview ## Key Developments - **Project Merger**: The fp-ts project is officially merging with the Effect-TS ecosystem. Giulio Canti, the author of fp-ts, is being welcomed into the Effect organization. For more details, see the [announcement here](https://dev.to/effect/a-bright-future-for-effect-455m). - **Continuity and Evolution**: Effect can be seen as the successor to fp-ts v2 and is effectively fp-ts v3, marking a significant evolution in the library's capabilities. ## FAQ ### Bundle Size Comparison Between Effect and fp-ts **Q: I compared the bundle sizes of two simple programs using Effect and fp-ts. Why does Effect have a larger bundle size?** A: It's natural to observe different bundle sizes because Effect and fp-ts are distinct systems designed for different purposes. Effect's bundle size is larger due to its included fiber runtime, which is crucial for its functionality. While the initial bundle size may seem large, the overhead amortizes as you use Effect. **Q: Should I be concerned about the bundle size difference when choosing between Effect and fp-ts?** A: Not necessarily. Consider the specific requirements and benefits of each library for your project. The **Micro** module in Effect is designed as a lightweight alternative to the standard `Effect` module, specifically for scenarios where reducing bundle size is crucial. This module is self-contained and does not include more complex features like `Layer`, `Ref`, `Queue`, and `Deferred`. If any major Effect modules (beyond basic data modules like `Option`, `Either`, `Array`, etc.) are used, the effect runtime will be added to your bundle, negating the benefits of Micro. This makes Micro ideal for libraries that aim to implement Effect functionality with minimal impact on bundle size, especially for libraries that plan to expose `Promise`-based APIs. It also supports scenarios where a client might use Micro while a server uses the full suite of Effect features, maintaining compatibility and shared logic between different parts of an application. ## Comparison Table The following table compares the features of the Effect and [fp-ts](https://github.com/gcanti/fp-ts) libraries. | Feature | fp-ts | Effect | | ------------------------- | ----- | ------ | | Typed Services | ❌ | ✅ | | Built-in Services | ❌ | ✅ | | Typed errors | ✅ | ✅ | | Pipeable APIs | ✅ | ✅ | | Dual APIs | ❌ | ✅ | | Testability | ❌ | ✅ | | Resource Management | ❌ | ✅ | | Interruptions | ❌ | ✅ | | Defects | ❌ | ✅ | | Fiber-Based Concurrency | ❌ | ✅ | | Fiber Supervision | ❌ | ✅ | | Retry and Retry Policies | ❌ | ✅ | | Built-in Logging | ❌ | ✅ | | Built-in Scheduling | ❌ | ✅ | | Built-in Caching | ❌ | ✅ | | Built-in Batching | ❌ | ✅ | | Metrics | ❌ | ✅ | | Tracing | ❌ | ✅ | | Configuration | ❌ | ✅ | | Immutable Data Structures | ❌ | ✅ | | Stream Processing | ❌ | ✅ | Here's an explanation of each feature: ### Typed Services Both fp-ts and Effect libraries provide the ability to track requirements at the type level, allowing you to define and use services with specific types. In fp-ts, you can utilize the `ReaderTaskEither` type, while in Effect, the `Effect` type is available. It's important to note that in fp-ts, the `R` type parameter is contravariant, which means that there is no guarantee of avoiding conflicts, and the library offers only basic tools for dependency management. On the other hand, in Effect, the `R` type parameter is covariant and all APIs have the ability to merge dependencies at the type level when multiple effects are involved. Effect also provides a range of specifically designed tools to simplify the management of dependencies, including `Tag`, `Context`, and `Layer`. These tools enhance the ease and flexibility of handling dependencies in your code, making it easier to compose and manage complex applications. ### Built-in Services The Effect library has built-in services like `Clock`, `Random` and `Tracer`, while fp-ts does not provide any default services. ### Typed errors Both libraries support typed errors, enabling you to define and handle errors with specific types. However, in Effect, all APIs have the ability to merge errors at the type-level when multiple effects are involved, and each effect can potentially fail with different types of errors. This means that when combining multiple effects that can fail, the resulting type of the error will be a union of the individual error types. Effect provides utilities and type-level operations to handle and manage these merged error types effectively. ### Pipeable APIs Both fp-ts and Effect libraries provide pipeable APIs, allowing you to compose and sequence operations in a functional and readable manner using the `pipe` function. However, Effect goes a step further and offers a `.pipe()` method on each data type, making it more convenient to work with pipelines without the need to explicitly import the `pipe` function every time. ### Dual APIs Effect library provides dual APIs, allowing you to use the same API in different ways (e.g., "data-last" and "data-first" variants). ### Testability The functional style of fp-ts generally promotes good testability of the code written using it, but the library itself does not provide dedicated tools specifically designed for the testing phase. On the other hand, Effect takes testability a step further by offering additional tools that are specifically tailored to simplify the testing process. Effect provides a range of utilities that improve testability. For example, it offers the `TestClock` utility, which allows you to control the passage of time during tests. This is useful for testing time-dependent code. Additionally, Effect provides the `TestRandom` utility, which enables fully deterministic testing of code that involves randomness. This ensures consistent and predictable test results. Another helpful tool is `ConfigProvider.fromMap`, which makes it easy to define mock configurations for your application during testing. ### Resource Management The Effect library provides built-in capabilities for resource management, while fp-ts has limited features in this area (mainly `bracket`) and they are less sophisticated. In Effect, resource management refers to the ability to acquire and release resources, such as database connections, file handles, or network sockets, in a safe and controlled manner. The library offers comprehensive and refined mechanisms to handle resource acquisition and release, ensuring proper cleanup and preventing resource leaks. ### Interruptions The Effect library supports interruptions, which means you can interrupt and cancel ongoing computations if needed. This feature gives you more control over the execution of your code and allows you to handle situations where you want to stop a computation before it completes. In Effect, interruptions are useful in scenarios where you need to handle user cancellations, timeouts, or other external events that require stopping ongoing computations. You can explicitly request an interruption and the library will safely and efficiently halt the execution of the computation. On the other hand, fp-ts does not have built-in support for interruptions. Once a computation starts in fp-ts, it will continue until it completes or encounters an error, without the ability to be interrupted midway. ### Defects The Effect library provides mechanisms for handling defects and managing **unexpected** failures. In Effect, defects refer to unexpected errors or failures that can occur during the execution of a program. With the Effect library, you have built-in tools and utilities to handle defects in a structured and reliable manner. It offers error handling capabilities that allow you to catch and handle exceptions, recover from failures, and gracefully handle unexpected scenarios. On the other hand, fp-ts does not have built-in support specifically dedicated to managing defects. While you can handle errors using standard functional programming techniques in fp-ts, the Effect library provides a more comprehensive and streamlined approach to dealing with defects. ### Fiber-Based Concurrency The Effect library leverages fiber-based concurrency, which enables lightweight and efficient concurrent computations. In simpler terms, fiber-based concurrency allows multiple tasks to run concurrently, making your code more responsive and efficient. With fiber-based concurrency, the Effect library can handle concurrent operations in a way that is lightweight and doesn't block the execution of other tasks. This means that you can run multiple computations simultaneously, taking advantage of the available resources and maximizing performance. On the other hand, fp-ts does not have built-in support for fiber-based concurrency. While fp-ts provides a rich set of functional programming features, it doesn't have the same level of support for concurrent computations as the Effect library. ### Fiber Supervision Effect library provides supervision strategies for managing and monitoring fibers. fp-ts does not have built-in support for fiber supervision. ### Retry and Retry Policies The Effect library includes built-in support for retrying computations with customizable retry policies. This feature is not available in fp-ts out of the box, and you would need to rely on external libraries to achieve similar functionality. However, it's important to note that the external libraries may not offer the same level of sophistication and fine-tuning as the built-in retry capabilities provided by the Effect library. Retry functionality allows you to automatically retry a computation or action when it fails, based on a set of predefined rules or policies. This can be particularly useful in scenarios where you are working with unreliable or unpredictable resources, such as network requests or external services. The Effect library provides a comprehensive set of retry policies that you can customize to fit your specific needs. These policies define the conditions for retrying a computation, such as the number of retries, the delay between retries, and the criteria for determining if a retry should be attempted. By leveraging the built-in retry functionality in the Effect library, you can handle transient errors or temporary failures in a more robust and resilient manner. This can help improve the overall reliability and stability of your applications, especially in scenarios where you need to interact with external systems or services. In contrast, fp-ts does not offer built-in support for retrying computations. If you require retry functionality in fp-ts, you would need to rely on external libraries, which may not provide the same level of sophistication and flexibility as the Effect library. It's worth noting that the built-in retry capabilities of the Effect library are designed to work seamlessly with its other features, such as error handling and resource management. This integration allows for a more cohesive and comprehensive approach to handling failures and retries within your computations. ### Built-in Logging The Effect library comes with built-in logging capabilities. This means that you can easily incorporate logging into your applications without the need for additional libraries or dependencies. In addition, the default logger provided by Effect can be replaced with a custom logger to suit your specific logging requirements. Logging is an essential aspect of software development as it allows you to record and track important information during the execution of your code. It helps you monitor the behavior of your application, debug issues, and gather insights for analysis. With the built-in logging capabilities of the Effect library, you can easily log messages, warnings, errors, or any other relevant information at various points in your code. This can be particularly useful for tracking the flow of execution, identifying potential issues, or capturing important events during the operation of your application. On the other hand, fp-ts does not provide built-in logging capabilities. If you need logging functionality in fp-ts, you would need to rely on external libraries or implement your own logging solution from scratch. This can introduce additional complexity and dependencies into your codebase. ### Built-in Scheduling The Effect library provides built-in scheduling capabilities, which allows you to manage the execution of computations over time. This feature is not available in fp-ts. In many applications, it's common to have tasks or computations that need to be executed at specific intervals or scheduled for future execution. For example, you might want to perform periodic data updates, trigger notifications, or run background processes at specific times. This is where built-in scheduling comes in handy. On the other hand, fp-ts does not have built-in scheduling capabilities. If you need to schedule tasks or manage timed computations in fp-ts, you would have to rely on external libraries or implement your own scheduling mechanisms, which can add complexity to your codebase. ### Built-in Caching The Effect library provides built-in caching mechanisms, which enable you to cache the results of computations for improved performance. This feature is not available in fp-ts. In many applications, computations can be time-consuming or resource-intensive, especially when dealing with complex operations or accessing remote resources. Caching is a technique used to store the results of computations so that they can be retrieved quickly without the need to recompute them every time. With the built-in caching capabilities of the Effect library, you can easily cache the results of computations and reuse them when needed. This can significantly improve the performance of your application by avoiding redundant computations and reducing the load on external resources. ### Built-in Batching The Effect library offers built-in batching capabilities, which enable you to combine multiple computations into a single batched computation. This feature is not available in fp-ts. In many scenarios, you may need to perform multiple computations that share similar inputs or dependencies. Performing these computations individually can result in inefficiencies and increased overhead. Batching is a technique that allows you to group these computations together and execute them as a single batch, improving performance and reducing unnecessary processing. ### Metrics The Effect library includes built-in support for collecting and reporting metrics related to computations and system behavior. It specifically supports [OpenTelemetry Metrics](https://opentelemetry.io/docs/specs/otel/metrics/). This feature is not available in fp-ts. Metrics play a crucial role in understanding and monitoring the performance and behavior of your applications. They provide valuable insights into various aspects, such as response times, resource utilization, error rates, and more. By collecting and analyzing metrics, you can identify performance bottlenecks, optimize your code, and make informed decisions to improve your application's overall quality. ### Tracing The Effect library has built-in tracing capabilities, which enable you to trace and debug the execution of your code and track the path of a request through an application. Additionally, Effect offers a dedicated [OpenTelemetry exporter](https://opentelemetry.io/docs/instrumentation/js/exporters/) for integrating with the OpenTelemetry observability framework. In contrast, fp-ts does not offer a similar tracing tool to enhance visibility into code execution. ### Configuration The Effect library provides built-in support for managing and accessing configuration values within your computations. This feature is not available in fp-ts. Configuration values are an essential aspect of software development. They allow you to customize the behavior of your applications without modifying the code. Examples of configuration values include database connection strings, API endpoints, feature flags, and various settings that can vary between environments or deployments. With the Effect library's built-in support for configuration, you can easily manage and access these values within your computations. It provides convenient utilities and abstractions to load, validate, and access configuration values, ensuring that your application has the necessary settings it requires to function correctly. By leveraging the built-in configuration support in the Effect library, you can: - Load configuration values from various sources such as environment variables, configuration files, or remote configuration providers. - Validate and ensure that the loaded configuration values adhere to the expected format and structure. - Access the configuration values within your computations, allowing you to use them wherever necessary. ### Immutable Data Structures The Effect library provides built-in support for immutable data structures such as `Chunk`, `HashSet`, and `HashMap`. These data structures ensure that once created, their values cannot be modified, promoting safer and more predictable code. In contrast, fp-ts does not have built-in support for such data structures and only provides modules that add additional APIs to standard data types like `Set` and `Map`. While these modules can be useful, they do not offer the same level of performance optimizations and specialized operations as the built-in immutable data structures provided by the Effect library. Immutable data structures offer several benefits, including: - Immutability: Immutable data structures cannot be changed after they are created. This property eliminates the risk of accidental modifications and enables safer concurrent programming. - Predictability: With immutable data structures, you can rely on the fact that their values won't change unexpectedly. This predictability simplifies reasoning about code behavior and reduces bugs caused by mutable state. - Sharing and Reusability: Immutable data structures can be safely shared between different parts of your program. Since they cannot be modified, you don't need to create defensive copies, resulting in more efficient memory usage and improved performance. ### Stream Processing The Effect ecosystem provides built-in support for stream processing, enabling you to work with streams of data. Stream processing is a powerful concept that allows you to efficiently process and transform continuous streams of data in a reactive and asynchronous manner. However, fp-ts does not have this feature built-in and relies on external libraries like RxJS to handle stream processing. # [Effect vs Promise](https://effect.website/docs/additional-resources/effect-vs-promise/) ## Overview import { Tabs, TabItem } from "@astrojs/starlight/components" In this guide, we will explore the differences between `Promise` and `Effect`, two approaches to handling asynchronous operations in TypeScript. We'll discuss their type safety, creation, chaining, and concurrency, providing examples to help you understand their usage. ## Comparing Effects and Promises: Key Distinctions - **Evaluation Strategy:** Promises are eagerly evaluated, whereas effects are lazily evaluated. - **Execution Mode:** Promises are one-shot, executing once, while effects are multi-shot, repeatable. - **Interruption Handling and Automatic Propagation:** Promises lack built-in interruption handling, posing challenges in managing interruptions, and don't automatically propagate interruptions, requiring manual abort controller management. In contrast, effects come with interruption handling capabilities and automatically compose interruption, simplifying management locally on smaller computations without the need for high-level orchestration. - **Structured Concurrency:** Effects offer structured concurrency built-in, which is challenging to achieve with Promises. - **Error Reporting (Type Safety):** Promises don't inherently provide detailed error reporting at the type level, whereas effects do, offering type-safe insight into error cases. - **Runtime Behavior:** The Effect runtime aims to remain synchronous as long as possible, transitioning into asynchronous mode only when necessary due to computation requirements or main thread starvation. ## Type safety Let's start by comparing the types of `Promise` and `Effect`. The type parameter `A` represents the resolved value of the operation: ```ts showLineNumbers=false Promise ``` ```ts showLineNumbers=false Effect ``` Here's what sets `Effect` apart: - It allows you to track the types of errors statically through the type parameter `Error`. For more information about error management in `Effect`, see [Expected Errors](/docs/error-management/expected-errors/). - It allows you to track the types of required dependencies statically through the type parameter `Context`. For more information about context management in `Effect`, see [Managing Services](/docs/requirements-management/services/). ## Creating ### Success Let's compare creating a successful operation using `Promise` and `Effect`: ```ts twoslash const success = Promise.resolve(2) ``` ```ts twoslash import { Effect } from "effect" const success = Effect.succeed(2) ``` ### Failure Now, let's see how to handle failures with `Promise` and `Effect`: ```ts twoslash const failure = Promise.reject("Uh oh!") ``` ```ts twoslash import { Effect } from "effect" const failure = Effect.fail("Uh oh!") ``` ### Constructor Creating operations with custom logic: ```ts twoslash const task = new Promise((resolve, reject) => { setTimeout(() => { Math.random() > 0.5 ? resolve(2) : reject("Uh oh!") }, 300) }) ``` ```ts twoslash import { Effect } from "effect" const task = Effect.gen(function* () { yield* Effect.sleep("300 millis") return Math.random() > 0.5 ? 2 : yield* Effect.fail("Uh oh!") }) ``` ## Thenable Mapping the result of an operation: ### map ```ts twoslash const mapped = Promise.resolve("Hello").then((s) => s.length) ``` ```ts twoslash import { Effect } from "effect" const mapped = Effect.succeed("Hello").pipe( Effect.map((s) => s.length) // or Effect.andThen((s) => s.length) ) ``` ### flatMap Chaining multiple operations: ```ts twoslash const flatMapped = Promise.resolve("Hello").then((s) => Promise.resolve(s.length) ) ``` ```ts twoslash import { Effect } from "effect" const flatMapped = Effect.succeed("Hello").pipe( Effect.flatMap((s) => Effect.succeed(s.length)) // or Effect.andThen((s) => Effect.succeed(s.length)) ) ``` ## Comparing Effect.gen with async/await If you are familiar with `async`/`await`, you may notice that the flow of writing code is similar. Let's compare the two approaches: ```ts twoslash const increment = (x: number) => x + 1 const divide = (a: number, b: number): Promise => b === 0 ? Promise.reject(new Error("Cannot divide by zero")) : Promise.resolve(a / b) const task1 = Promise.resolve(10) const task2 = Promise.resolve(2) const program = async function () { const a = await task1 const b = await task2 const n1 = await divide(a, b) const n2 = increment(n1) return `Result is: ${n2}` } program().then(console.log) // Output: "Result is: 6" ``` ```ts twoslash import { Effect } from "effect" const increment = (x: number) => x + 1 const divide = (a: number, b: number): Effect.Effect => b === 0 ? Effect.fail(new Error("Cannot divide by zero")) : Effect.succeed(a / b) const task1 = Effect.promise(() => Promise.resolve(10)) const task2 = Effect.promise(() => Promise.resolve(2)) const program = Effect.gen(function* () { const a = yield* task1 const b = yield* task2 const n1 = yield* divide(a, b) const n2 = increment(n1) return `Result is: ${n2}` }) Effect.runPromise(program).then(console.log) // Output: "Result is: 6" ``` It's important to note that although the code appears similar, the two programs are not identical. The purpose of comparing them side by side is just to highlight the resemblance in how they are written. ## Concurrency ### Promise.all() ```ts twoslash const task1 = new Promise((resolve, reject) => { console.log("Executing task1...") setTimeout(() => { console.log("task1 done") resolve(1) }, 100) }) const task2 = new Promise((resolve, reject) => { console.log("Executing task2...") setTimeout(() => { console.log("task2 done") reject("Uh oh!") }, 200) }) const task3 = new Promise((resolve, reject) => { console.log("Executing task3...") setTimeout(() => { console.log("task3 done") resolve(3) }, 300) }) const program = Promise.all([task1, task2, task3]) program.then(console.log, console.error) /* Output: Executing task1... Executing task2... Executing task3... task1 done task2 done Uh oh! task3 done */ ``` ```ts twoslash import { Effect } from "effect" const task1 = Effect.gen(function* () { console.log("Executing task1...") yield* Effect.sleep("100 millis") console.log("task1 done") return 1 }) const task2 = Effect.gen(function* () { console.log("Executing task2...") yield* Effect.sleep("200 millis") console.log("task2 done") return yield* Effect.fail("Uh oh!") }) const task3 = Effect.gen(function* () { console.log("Executing task3...") yield* Effect.sleep("300 millis") console.log("task3 done") return 3 }) const program = Effect.all([task1, task2, task3], { concurrency: "unbounded" }) Effect.runPromise(program).then(console.log, console.error) /* Output: Executing task1... Executing task2... Executing task3... task1 done task2 done (FiberFailure) Error: Uh oh! */ ``` ### Promise.allSettled() ```ts const task1 = new Promise((resolve, reject) => { console.log("Executing task1...") setTimeout(() => { console.log("task1 done") resolve(1) }, 100) }) const task2 = new Promise((resolve, reject) => { console.log("Executing task2...") setTimeout(() => { console.log("task2 done") reject("Uh oh!") }, 200) }) const task3 = new Promise((resolve, reject) => { console.log("Executing task3...") setTimeout(() => { console.log("task3 done") resolve(3) }, 300) }) const program = Promise.allSettled([task1, task2, task3]) program.then(console.log, console.error) /* Output: Executing task1... Executing task2... Executing task3... task1 done task2 done task3 done [ { status: 'fulfilled', value: 1 }, { status: 'rejected', reason: 'Uh oh!' }, { status: 'fulfilled', value: 3 } ] */ ``` ```ts twoslash import { Effect } from "effect" const task1 = Effect.gen(function* () { console.log("Executing task1...") yield* Effect.sleep("100 millis") console.log("task1 done") return 1 }) const task2 = Effect.gen(function* () { console.log("Executing task2...") yield* Effect.sleep("200 millis") console.log("task2 done") return yield* Effect.fail("Uh oh!") }) const task3 = Effect.gen(function* () { console.log("Executing task3...") yield* Effect.sleep("300 millis") console.log("task3 done") return 3 }) const program = Effect.forEach( [task1, task2, task3], (task) => Effect.either(task), // or Effect.exit { concurrency: "unbounded" } ) Effect.runPromise(program).then(console.log, console.error) /* Output: Executing task1... Executing task2... Executing task3... task1 done task2 done task3 done [ { _id: "Either", _tag: "Right", right: 1 }, { _id: "Either", _tag: "Left", left: "Uh oh!" }, { _id: "Either", _tag: "Right", right: 3 } ] */ ``` ### Promise.any() ```ts const task1 = new Promise((resolve, reject) => { console.log("Executing task1...") setTimeout(() => { console.log("task1 done") reject("Something went wrong!") }, 100) }) const task2 = new Promise((resolve, reject) => { console.log("Executing task2...") setTimeout(() => { console.log("task2 done") resolve(2) }, 200) }) const task3 = new Promise((resolve, reject) => { console.log("Executing task3...") setTimeout(() => { console.log("task3 done") reject("Uh oh!") }, 300) }) const program = Promise.any([task1, task2, task3]) program.then(console.log, console.error) /* Output: Executing task1... Executing task2... Executing task3... task1 done task2 done 2 task3 done */ ``` ```ts twoslash import { Effect } from "effect" const task1 = Effect.gen(function* () { console.log("Executing task1...") yield* Effect.sleep("100 millis") console.log("task1 done") return yield* Effect.fail("Something went wrong!") }) const task2 = Effect.gen(function* () { console.log("Executing task2...") yield* Effect.sleep("200 millis") console.log("task2 done") return 2 }) const task3 = Effect.gen(function* () { console.log("Executing task3...") yield* Effect.sleep("300 millis") console.log("task3 done") return yield* Effect.fail("Uh oh!") }) const program = Effect.raceAll([task1, task2, task3]) Effect.runPromise(program).then(console.log, console.error) /* Output: Executing task1... Executing task2... Executing task3... task1 done task2 done 2 */ ``` ### Promise.race() ```ts twoslash const task1 = new Promise((resolve, reject) => { console.log("Executing task1...") setTimeout(() => { console.log("task1 done") reject("Something went wrong!") }, 100) }) const task2 = new Promise((resolve, reject) => { console.log("Executing task2...") setTimeout(() => { console.log("task2 done") reject("Uh oh!") }, 200) }) const task3 = new Promise((resolve, reject) => { console.log("Executing task3...") setTimeout(() => { console.log("task3 done") resolve(3) }, 300) }) const program = Promise.race([task1, task2, task3]) program.then(console.log, console.error) /* Output: Executing task1... Executing task2... Executing task3... task1 done Something went wrong! task2 done task3 done */ ``` ```ts twoslash import { Effect } from "effect" const task1 = Effect.gen(function* () { console.log("Executing task1...") yield* Effect.sleep("100 millis") console.log("task1 done") return yield* Effect.fail("Something went wrong!") }) const task2 = Effect.gen(function* () { console.log("Executing task2...") yield* Effect.sleep("200 millis") console.log("task2 done") return yield* Effect.fail("Uh oh!") }) const task3 = Effect.gen(function* () { console.log("Executing task3...") yield* Effect.sleep("300 millis") console.log("task3 done") return 3 }) const program = Effect.raceAll([task1, task2, task3].map(Effect.either)) // or Effect.exit Effect.runPromise(program).then(console.log, console.error) /* Output: Executing task1... Executing task2... Executing task3... task1 done { _id: "Either", _tag: "Left", left: "Something went wrong!" } */ ``` ## FAQ **Question**. What is the equivalent of starting a promise without immediately waiting for it in Effects? ```ts {10,16} twoslash const task = (delay: number, name: string) => new Promise((resolve) => setTimeout(() => { console.log(`${name} done`) return resolve(name) }, delay) ) export async function program() { const r0 = task(2_000, "long running task") const r1 = await task(200, "task 2") const r2 = await task(100, "task 3") return { r1, r2, r0: await r0 } } program().then(console.log) /* Output: task 2 done task 3 done long running task done { r1: 'task 2', r2: 'task 3', r0: 'long running promise' } */ ``` **Answer:** You can achieve this by utilizing `Effect.fork` and `Fiber.join`. ```ts {11,17} twoslash import { Effect, Fiber } from "effect" const task = (delay: number, name: string) => Effect.gen(function* () { yield* Effect.sleep(delay) console.log(`${name} done`) return name }) const program = Effect.gen(function* () { const r0 = yield* Effect.fork(task(2_000, "long running task")) const r1 = yield* task(200, "task 2") const r2 = yield* task(100, "task 3") return { r1, r2, r0: yield* Fiber.join(r0) } }) Effect.runPromise(program).then(console.log) /* Output: task 2 done task 3 done long running task done { r1: 'task 2', r2: 'task 3', r0: 'long running promise' } */ ``` # [Myths About Effect](https://effect.website/docs/additional-resources/myths/) ## Overview ## Effect heavily relies on generators and generators are slow! Effect's internals are not built on generators, we only use generators to provide an API which closely mimics async-await. Internally async-await uses the same mechanics as generators and they are equally performant. So if you don't have a problem with async-await you won't have a problem with Effect's generators. Where generators and iterables are unacceptably slow is in transforming collections of data, for that try to use plain arrays as much as possible. ## Effect will make your code 500x slower! Effect does perform 500x slower if you are comparing: ```ts twoslash const result = 1 + 1 ``` to ```ts twoslash import { Effect } from "effect" const result = Effect.runSync( Effect.zipWith(Effect.succeed(1), Effect.succeed(1), (a, b) => a + b) ) ``` The reason is one operation is optimized by the JIT compiler to be a direct CPU instruction and the other isn't. In reality you'd never use Effect in such cases, Effect is an app-level library to tame concurrency, error handling, and much more! You'd use Effect to coordinate your thunks of code, and you can build your thunks of code in the best perfoming manner as you see fit while still controlling execution through Effect. ## Effect has a huge performance overhead! Depends what you mean by performance, many times performance bottlenecks in JS are due to bad management of concurrency. Thanks to structured concurrency and observability it becomes much easier to spot and optimize those issues. There are apps in frontend running at 120fps that use Effect intensively, so most likely effect won't be your perf problem. In regards of memory, it doesn't use much more memory than a normal program would, there are a few more allocations compared to non Effect code but usually this is no longer the case when the non Effect code does the same thing as the Effect code. The advise would be start using it and monitor your code, optimise out of need not out of thought, optimizing too early is the root of all evils in software design. ## The bundle size is HUGE! Effect's minimum cost is about 25k of gzipped code, that chunk contains the Effect Runtime and already includes almost all the functions that you'll need in a normal app-code scenario. From that point on Effect is tree-shaking friendly so you'll only include what you use. Also when using Effect your own code becomes shorter and terser, so the overall cost is amortized with usage, we have apps where adopting Effect in the majority of the codebase led to reduction of the final bundle. ## Effect is impossible to learn, there are so many functions and modules! True, the full Effect ecosystem is quite large and some modules contain 1000s of functions, the reality is that you don't need to know them all to start being productive, you can safely start using Effect knowing just 10-20 functions and progressively discover the rest, just like you can start using TypeScript without knowing every single NPM package. A short list of commonly used functions to begin are: - [Effect.succeed](/docs/getting-started/creating-effects/#succeed) - [Effect.fail](/docs/getting-started/creating-effects/#fail) - [Effect.sync](/docs/getting-started/creating-effects/#sync) - [Effect.tryPromise](/docs/getting-started/creating-effects/#trypromise) - [Effect.gen](/docs/getting-started/using-generators/) - [Effect.runPromise](/docs/getting-started/running-effects/#runpromise) - [Effect.catchTag](/docs/error-management/expected-errors/#catchtag) - [Effect.catchAll](/docs/error-management/expected-errors/#catchall) - [Effect.acquireRelease](/docs/resource-management/scope/#acquirerelease) - [Effect.acquireUseRelease](/docs/resource-management/introduction/#acquireuserelease) - [Effect.provide](/docs/requirements-management/layers/#providing-a-layer-to-an-effect) - [Effect.provideService](/docs/requirements-management/services/#providing-a-service-implementation) - [Effect.andThen](/docs/getting-started/building-pipelines/#andthen) - [Effect.map](/docs/getting-started/building-pipelines/#map) - [Effect.tap](/docs/getting-started/building-pipelines/#tap) A short list of commonly used modules: - [Effect](https://effect-ts.github.io/effect/effect/Effect.ts.html) - [Context](/docs/requirements-management/services/#creating-a-service) - [Layer](/docs/requirements-management/layers/) - [Option](/docs/data-types/option/) - [Either](/docs/data-types/either/) - [Array](https://effect-ts.github.io/effect/effect/Array.ts.html) - [Match](/docs/code-style/pattern-matching/) ## Effect is the same as RxJS and shares its problems This is a sensitive topic, let's start by saying that RxJS is a great project and that it has helped millions of developers write reliable software and we all should be thankful to the developers who contributed to such an amazing project. Discussing the scope of the projects, RxJS aims to make working with Observables easy and wants to provide reactive extensions to JS, Effect instead wants to make writing production-grade TypeScript easy. While the intersection is non-empty the projects have fundamentally different objectives and strategies. Sometimes people refer to RxJS in bad light, and the reason isn't RxJS in itself but rather usage of RxJS in problem domains where RxJS wasn't thought to be used. Namely the idea that "everything is a stream" is theoretically true but it leads to fundamental limitations on developer experience, the primary issue being that streams are multi-shot (emit potentially multiple elements, or zero) and mutable delimited continuations (JS Generators) are known to be only good to represent single-shot effects (that emit a single value). In short it means that writing in imperative style (think of async/await) is practically impossible with stream primitives (practically because there would be the option of replaying the generator at every element and at every step, but this tends to be inefficient and the semantics of it are counter-intuitive, it would only work under the assumption that the full body is free of side-effects), forcing the developer to use declarative approaches such as pipe to represent all of their code. Effect has a Stream module (which is pull-based instead of push-based in order to be memory constant), but the basic Effect type is single-shot and it is optimised to act as a smart & lazy Promise that enables imperative programming, so when using Effect you're not forced to use a declarative style for everything and you can program using a model which is similar to async-await. The other big difference is that RxJS only cares about the happy-path with explicit types, it doesn't offer a way of typing errors and dependencies, Effect instead consider both errors and dependencies as explicitely typed and offers control-flow around those in a fully type-safe manner. In short if you need reactive programming around Observables, use RxJS, if you need to write production-grade TypeScript that includes by default native telemetry, error handling, dependency injection, and more use Effect. ## Effect should be a language or Use a different language Neither solve the issue of writing production grade software in TypeScript. TypeScript is an amazing language to write full stack code with deep roots in the JS ecosystem and wide compatibility of tools, it is an industrial language adopted by many large scale companies. The fact that something like Effect is possible within the language and the fact that the language supports things such as generators that allows for imperative programming with custom types such as Effect makes TypeScript a unique language. In fact even in functional languages such as Scala the interop with effect systems is less optimal than it is in TypeScript, to the point that effect system authors have expressed wish for their language to support as much as TypeScript supports. # [Equivalence](https://effect.website/docs/behaviour/equivalence/) ## Overview The Equivalence module provides a way to define equivalence relations between values in TypeScript. An equivalence relation is a binary relation that is reflexive, symmetric, and transitive, establishing a formal notion of when two values should be considered equivalent. ## What is Equivalence? An `Equivalence` represents a function that compares two values of type `A` and determines if they are equivalent. This is more flexible and customizable than simple equality checks using `===`. Here's the structure of an `Equivalence`: ```ts showLineNumbers=false interface Equivalence { (self: A, that: A): boolean } ``` ## Using Built-in Equivalences The module provides several built-in equivalence relations for common data types: | Equivalence | Description | | ----------- | ------------------------------------------- | | `string` | Uses strict equality (`===`) for strings | | `number` | Uses strict equality (`===`) for numbers | | `boolean` | Uses strict equality (`===`) for booleans | | `symbol` | Uses strict equality (`===`) for symbols | | `bigint` | Uses strict equality (`===`) for bigints | | `Date` | Compares `Date` objects by their timestamps | **Example** (Using Built-in Equivalences) ```ts twoslash import { Equivalence } from "effect" console.log(Equivalence.string("apple", "apple")) // Output: true console.log(Equivalence.string("apple", "orange")) // Output: false console.log(Equivalence.Date(new Date(2023, 1, 1), new Date(2023, 1, 1))) // Output: true console.log(Equivalence.Date(new Date(2023, 1, 1), new Date(2023, 10, 1))) // Output: false ``` ## Deriving Equivalences For more complex data structures, you may need custom equivalences. The Equivalence module lets you derive new `Equivalence` instances from existing ones with the `Equivalence.mapInput` function. **Example** (Creating a Custom Equivalence for Objects) ```ts twoslash import { Equivalence } from "effect" interface User { readonly id: number readonly name: string } // Create an equivalence that compares User objects based only on the id const equivalence = Equivalence.mapInput( Equivalence.number, // Base equivalence for comparing numbers (user: User) => user.id // Function to extract the id from a User ) // Compare two User objects: they are equivalent if their ids are the same console.log(equivalence({ id: 1, name: "Alice" }, { id: 1, name: "Al" })) // Output: true ``` The `Equivalence.mapInput` function takes two arguments: 1. The existing `Equivalence` you want to use as a base (`Equivalence.number` in this case, for comparing numbers). 2. A function that extracts the value used for the equivalence check from your data structure (`(user: User) => user.id` in this case). # [Order](https://effect.website/docs/behaviour/order/) ## Overview The Order module provides a way to compare values and determine their order. It defines an interface `Order` which represents a single function for comparing two values of type `A`. The function returns `-1`, `0`, or `1`, indicating whether the first value is less than, equal to, or greater than the second value. Here's the basic structure of an `Order`: ```ts showLineNumbers=false interface Order { (first: A, second: A): -1 | 0 | 1 } ``` ## Using the Built-in Orders The Order module comes with several built-in comparators for common data types: | Order | Description | | -------- | ---------------------------------- | | `string` | Used for comparing strings. | | `number` | Used for comparing numbers. | | `bigint` | Used for comparing big integers. | | `Date` | Used for comparing `Date` objects. | **Example** (Using Built-in Comparators) ```ts twoslash import { Order } from "effect" console.log(Order.string("apple", "banana")) // Output: -1, as "apple" < "banana" console.log(Order.number(1, 1)) // Output: 0, as 1 = 1 console.log(Order.bigint(2n, 1n)) // Output: 1, as 2n > 1n ``` ## Sorting Arrays You can sort arrays using these comparators. The `Array` module offers a `sort` function that sorts arrays without altering the original one. **Example** (Sorting Arrays with `Order`) ```ts twoslash import { Order, Array } from "effect" const strings = ["b", "a", "d", "c"] const result = Array.sort(strings, Order.string) console.log(strings) // Original array remains unchanged // Output: [ 'b', 'a', 'd', 'c' ] console.log(result) // Sorted array // Output: [ 'a', 'b', 'c', 'd' ] ``` You can also use an `Order` as a comparator with JavaScript's native `Array.sort` method, but keep in mind that this will modify the original array. **Example** (Using `Order` with Native `Array.prototype.sort`) ```ts twoslash import { Order } from "effect" const strings = ["b", "a", "d", "c"] strings.sort(Order.string) // Modifies the original array console.log(strings) // Output: [ 'a', 'b', 'c', 'd' ] ``` ## Deriving Orders For more complex data structures, you may need custom sorting rules. The Order module lets you derive new `Order` instances from existing ones with the `Order.mapInput` function. **Example** (Creating a Custom Order for Objects) Imagine you have a list of `Person` objects, and you want to sort them by their names in ascending order. To achieve this, you can create a custom `Order`. ```ts twoslash import { Order } from "effect" // Define the Person interface interface Person { readonly name: string readonly age: number } // Create a custom order to sort Person objects by name in ascending order // // ┌─── Order // ▼ const byName = Order.mapInput( Order.string, (person: Person) => person.name ) ``` The `Order.mapInput` function takes two arguments: 1. The existing `Order` you want to use as a base (`Order.string` in this case, for comparing strings). 2. A function that extracts the value you want to use for sorting from your data structure (`(person: Person) => person.name` in this case). Once you have defined your custom `Order`, you can apply it to sort an array of `Person` objects: **Example** (Sorting Objects Using a Custom Order) ```ts twoslash collapse={3-13} import { Order, Array } from "effect" // Define the Person interface interface Person { readonly name: string readonly age: number } // Create a custom order to sort Person objects by name in ascending order const byName = Order.mapInput( Order.string, (person: Person) => person.name ) const persons: ReadonlyArray = [ { name: "Charlie", age: 22 }, { name: "Alice", age: 25 }, { name: "Bob", age: 30 } ] // Sort persons array using the custom order const sortedPersons = Array.sort(persons, byName) console.log(sortedPersons) /* Output: [ { name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }, { name: 'Charlie', age: 22 } ] */ ``` ## Combining Orders The Order module lets you combine multiple `Order` instances to create complex sorting rules. This is useful when sorting by multiple properties. **Example** (Sorting by Multiple Criteria) Imagine you have a list of people, each represented by an object with a `name` and an `age`. You want to sort this list first by name and then, for individuals with the same name, by age. ```ts twoslash import { Order, Array } from "effect" // Define the Person interface interface Person { readonly name: string readonly age: number } // Create an Order to sort people by their names in ascending order const byName = Order.mapInput( Order.string, (person: Person) => person.name ) // Create an Order to sort people by their ages in ascending order const byAge = Order.mapInput(Order.number, (person: Person) => person.age) // Combine orders to sort by name, then by age const byNameAge = Order.combine(byName, byAge) const result = Array.sort( [ { name: "Bob", age: 20 }, { name: "Alice", age: 18 }, { name: "Bob", age: 18 } ], byNameAge ) console.log(result) /* Output: [ { name: 'Alice', age: 18 }, // Sorted by name { name: 'Bob', age: 18 }, // Sorted by age within the same name { name: 'Bob', age: 20 } ] */ ``` ## Additional Useful Functions The Order module provides additional functions for common comparison operations, making it easier to work with ordered values. ### Reversing Order `Order.reverse` inverts the order of comparison. If you have an `Order` for ascending values, reversing it makes it descending. **Example** (Reversing an Order) ```ts twoslash import { Order } from "effect" const ascendingOrder = Order.number const descendingOrder = Order.reverse(ascendingOrder) console.log(ascendingOrder(1, 3)) // Output: -1 (1 < 3 in ascending order) console.log(descendingOrder(1, 3)) // Output: 1 (1 > 3 in descending order) ``` ### Comparing Values These functions allow you to perform simple comparisons between values: | API | Description | | ---------------------- | -------------------------------------------------------- | | `lessThan` | Checks if one value is strictly less than another. | | `greaterThan` | Checks if one value is strictly greater than another. | | `lessThanOrEqualTo` | Checks if one value is less than or equal to another. | | `greaterThanOrEqualTo` | Checks if one value is greater than or equal to another. | **Example** (Using Comparison Functions) ```ts twoslash import { Order } from "effect" console.log(Order.lessThan(Order.number)(1, 2)) // Output: true (1 < 2) console.log(Order.greaterThan(Order.number)(5, 3)) // Output: true (5 > 3) console.log(Order.lessThanOrEqualTo(Order.number)(2, 2)) // Output: true (2 <= 2) console.log(Order.greaterThanOrEqualTo(Order.number)(4, 4)) // Output: true (4 >= 4) ``` ### Finding Minimum and Maximum The `Order.min` and `Order.max` functions return the minimum or maximum value between two values, considering the order. **Example** (Finding Minimum and Maximum Numbers) ```ts twoslash import { Order } from "effect" console.log(Order.min(Order.number)(3, 1)) // Output: 1 (1 is the minimum) console.log(Order.max(Order.number)(5, 8)) // Output: 8 (8 is the maximum) ``` ### Clamping Values `Order.clamp` restricts a value within a given range. If the value is outside the range, it is adjusted to the nearest bound. **Example** (Clamping Numbers to a Range) ```ts twoslash import { Order } from "effect" // Define a function to clamp numbers between 20 and 30 const clampNumbers = Order.clamp(Order.number)({ minimum: 20, maximum: 30 }) // Value 26 is within the range [20, 30], so it remains unchanged console.log(clampNumbers(26)) // Output: 26 // Value 10 is below the minimum bound, so it is clamped to 20 console.log(clampNumbers(10)) // Output: 20 // Value 40 is above the maximum bound, so it is clamped to 30 console.log(clampNumbers(40)) // Output: 30 ``` ### Checking Value Range `Order.between` checks if a value falls within a specified inclusive range. **Example** (Checking if Numbers Fall Within a Range) ```ts twoslash import { Order } from "effect" // Create a function to check if numbers are between 20 and 30 const betweenNumbers = Order.between(Order.number)({ minimum: 20, maximum: 30 }) // Value 26 falls within the range [20, 30], so it returns true console.log(betweenNumbers(26)) // Output: true // Value 10 is below the minimum bound, so it returns false console.log(betweenNumbers(10)) // Output: false // Value 40 is above the maximum bound, so it returns false console.log(betweenNumbers(40)) // Output: false ``` # [Cache](https://effect.website/docs/caching/cache/) ## Overview In many applications, handling overlapping work is common. For example, in services that process incoming requests, it's important to avoid redundant work like handling the same request multiple times. The Cache module helps improve performance by preventing duplicate work. Key Features of Cache: | Feature | Description | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | **Compositionality** | Allows overlapping work across different parts of the application while preserving compositional programming. | | **Unified Sync and Async Caches** | Integrates both synchronous and asynchronous caches through a unified lookup function that computes values either way. | | **Effect Integration** | Works natively with the Effect library, supporting concurrent lookups, failure handling, and interruption. | | **Cache Metrics** | Tracks key metrics like entries, hits, and misses, providing insights for performance optimization. | ## Creating a Cache A cache is defined by a lookup function that computes the value for a given key if it's not already cached: ```ts showLineNumbers=false type Lookup = ( key: Key ) => Effect ``` The lookup function takes a `Key` and returns an `Effect`, which describes how to compute the value (`Value`). This `Effect` may require an environment (`Requirements`), can fail with an `Error`, and succeed with a `Value`. Since it returns an `Effect`, it can handle both synchronous and asynchronous workflows. You create a cache by providing a lookup function along with a maximum size and a time-to-live (TTL) for cached values. ```ts showLineNumbers=false declare const make: (options: { readonly capacity: number readonly timeToLive: Duration.DurationInput readonly lookup: Lookup }) => Effect, never, Requirements> ``` Once a cache is created, the most idiomatic way to work with it is the `get` method. The `get` method returns the current value in the cache if it exists, or computes a new value, puts it in the cache, and returns it. If multiple concurrent processes request the same value, it will only be computed once. All other processes will receive the computed value as soon as it is available. This is managed using Effect's fiber-based concurrency model without blocking the underlying thread. **Example** (Concurrent Cache Lookups) In this example, we call `timeConsumingEffect` three times concurrently with the same key. The cache runs this effect only once, so concurrent lookups will wait until the value is available: ```ts twoslash import { Effect, Cache, Duration } from "effect" // Simulating an expensive lookup with a delay const expensiveLookup = (key: string) => Effect.sleep("2 seconds").pipe(Effect.as(key.length)) const program = Effect.gen(function* () { // Create a cache with a capacity of 100 and an infinite TTL const cache = yield* Cache.make({ capacity: 100, timeToLive: Duration.infinity, lookup: expensiveLookup }) // Perform concurrent lookups using the same key const result = yield* Effect.all( [cache.get("key1"), cache.get("key1"), cache.get("key1")], { concurrency: "unbounded" } ) console.log( "Result of parallel execution of three effects" + `with the same key: ${result}` ) // Fetch and display cache stats const hits = yield* cache.cacheStats.pipe( Effect.map((stats) => stats.hits) ) console.log(`Number of cache hits: ${hits}`) const misses = yield* cache.cacheStats.pipe( Effect.map((stats) => stats.misses) ) console.log(`Number of cache misses: ${misses}`) }) Effect.runPromise(program) /* Output: Result of parallel execution of three effects with the same key: 4,4,4 Number of cache hits: 2 Number of cache misses: 1 */ ``` ## Concurrent Access The cache is designed to be safe for concurrent access and efficient under concurrent conditions. If two concurrent processes request the same value and it is not in the cache, the value will be computed once and provided to both processes as soon as it is available. Concurrent processes will wait for the value without blocking the underlying thread. If the lookup function fails or is interrupted, the error will be propagated to all concurrent processes waiting for the value. Failures are cached to prevent repeated computation of the same failed value. If interrupted, the key will be removed from the cache, so subsequent calls will attempt to compute the value again. ## Capacity A cache is created with a specified capacity. When the cache reaches capacity, the least recently accessed values will be removed first. The cache size may slightly exceed the specified capacity between operations. ## Time To Live (TTL) A cache can also have a specified time to live (TTL). Values older than the TTL will not be returned. The age is calculated from when the value was loaded into the cache. ## Methods In addition to `get`, the cache provides several other methods: | Method | Description | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `refresh` | Triggers a recomputation of the value for a key without removing the old value, allowing continued access. | | `size` | Returns the current size of the cache. The size is approximate under concurrent conditions. | | `contains` | Checks if a value associated with a specified key exists in the cache. Under concurrent access, the result is valid as of the check time but may change immediately after. | | `invalidate` | Evicts the value associated with a specific key. | | `invalidateAll` | Evicts all values from the cache. | # [Caching Effects](https://effect.website/docs/caching/caching-effects/) ## Overview This section covers several functions from the library that help manage caching and memoization in your application. ## cachedFunction Memoizes a function with effects, caching results for the same inputs to avoid recomputation. **Example** (Memoizing a Random Number Generator) ```ts twoslash import { Effect, Random } from "effect" const program = Effect.gen(function* () { const randomNumber = (n: number) => Random.nextIntBetween(1, n) console.log("non-memoized version:") console.log(yield* randomNumber(10)) // Generates a new random number console.log(yield* randomNumber(10)) // Generates a different number console.log("memoized version:") const memoized = yield* Effect.cachedFunction(randomNumber) console.log(yield* memoized(10)) // Generates and caches the result console.log(yield* memoized(10)) // Reuses the cached result }) Effect.runFork(program) /* Example Output: non-memoized version: 2 8 memoized version: 5 5 */ ``` ## once Ensures an effect is executed only once, even if invoked multiple times. **Example** (Single Execution of an Effect) ```ts twoslash import { Effect, Console } from "effect" const program = Effect.gen(function* () { const task1 = Console.log("task1") // Repeats task1 three times yield* Effect.repeatN(task1, 2) // Ensures task2 is executed only once const task2 = yield* Effect.once(Console.log("task2")) // Attempts to repeat task2, but it will only execute once yield* Effect.repeatN(task2, 2) }) Effect.runFork(program) /* Output: task1 task1 task1 task2 */ ``` ## cached Returns an effect that computes a result lazily and caches it. Subsequent evaluations of this effect will return the cached result without re-executing the logic. **Example** (Lazy Caching of an Expensive Task) ```ts twoslash import { Effect, Console } from "effect" let i = 1 // Simulating an expensive task with a delay const expensiveTask = Effect.promise(() => { console.log("expensive task...") return new Promise((resolve) => { setTimeout(() => { resolve(`result ${i++}`) }, 100) }) }) const program = Effect.gen(function* () { // Without caching, the task is executed each time console.log("-- non-cached version:") yield* expensiveTask.pipe(Effect.andThen(Console.log)) yield* expensiveTask.pipe(Effect.andThen(Console.log)) // With caching, the result is reused after the first run console.log("-- cached version:") const cached = yield* Effect.cached(expensiveTask) yield* cached.pipe(Effect.andThen(Console.log)) yield* cached.pipe(Effect.andThen(Console.log)) }) Effect.runFork(program) /* Output: -- non-cached version: expensive task... result 1 expensive task... result 2 -- cached version: expensive task... result 3 result 3 */ ``` ## cachedWithTTL Returns an effect that caches its result for a specified duration, known as the `timeToLive`. When the cache expires after the duration, the effect will be recomputed upon next evaluation. **Example** (Caching with Time-to-Live) ```ts twoslash import { Effect, Console } from "effect" let i = 1 // Simulating an expensive task with a delay const expensiveTask = Effect.promise(() => { console.log("expensive task...") return new Promise((resolve) => { setTimeout(() => { resolve(`result ${i++}`) }, 100) }) }) const program = Effect.gen(function* () { // Caches the result for 150 milliseconds const cached = yield* Effect.cachedWithTTL(expensiveTask, "150 millis") // First evaluation triggers the task yield* cached.pipe(Effect.andThen(Console.log)) // Second evaluation returns the cached result yield* cached.pipe(Effect.andThen(Console.log)) // Wait for 100 milliseconds, ensuring the cache expires yield* Effect.sleep("100 millis") // Recomputes the task after cache expiration yield* cached.pipe(Effect.andThen(Console.log)) }) Effect.runFork(program) /* Output: expensive task... result 1 result 1 expensive task... result 2 */ ``` ## cachedInvalidateWithTTL Similar to `Effect.cachedWithTTL`, this function caches an effect's result for a specified duration. It also includes an additional effect for manually invalidating the cached value before it naturally expires. **Example** (Invalidating Cache Manually) ```ts twoslash import { Effect, Console } from "effect" let i = 1 // Simulating an expensive task with a delay const expensiveTask = Effect.promise(() => { console.log("expensive task...") return new Promise((resolve) => { setTimeout(() => { resolve(`result ${i++}`) }, 100) }) }) const program = Effect.gen(function* () { // Caches the result for 150 milliseconds const [cached, invalidate] = yield* Effect.cachedInvalidateWithTTL( expensiveTask, "150 millis" ) // First evaluation triggers the task yield* cached.pipe(Effect.andThen(Console.log)) // Second evaluation returns the cached result yield* cached.pipe(Effect.andThen(Console.log)) // Invalidate the cache before it naturally expires yield* invalidate // Third evaluation triggers the task again // since the cache was invalidated yield* cached.pipe(Effect.andThen(Console.log)) }) Effect.runFork(program) /* Output: expensive task... result 1 result 1 expensive task... result 2 */ ``` # [Branded Types](https://effect.website/docs/code-style/branded-types/) ## Overview In this guide, we will explore the concept of **branded types** in TypeScript and learn how to create and work with them using the Brand module. Branded types are TypeScript types with an added type tag that helps prevent accidental usage of a value in the wrong context. They allow us to create distinct types based on an existing underlying type, enabling type safety and better code organization. ## The Problem with TypeScript's Structural Typing TypeScript's type system is structurally typed, meaning that two types are considered compatible if their members are compatible. This can lead to situations where values of the same underlying type are used interchangeably, even when they represent different concepts or have different meanings. Consider the following types: ```ts twoslash type UserId = number type ProductId = number ``` Here, `UserId` and `ProductId` are structurally identical as they are both based on `number`. TypeScript will treat these as interchangeable, potentially causing bugs if they are mixed up in your application. **Example** (Unintended Type Compatibility) ```ts twoslash type UserId = number type ProductId = number const getUserById = (id: UserId) => { // Logic to retrieve user } const getProductById = (id: ProductId) => { // Logic to retrieve product } const id: UserId = 1 getProductById(id) // No type error, but incorrect usage ``` In the example above, passing a `UserId` to `getProductById` does not produce a type error, even though it's logically incorrect. This happens because both types are considered interchangeable. ## How Branded Types Help Branded types allow you to create distinct types from the same underlying type by adding a unique type tag, enforcing proper usage at compile-time. Branding is accomplished by adding a symbolic identifier that distinguishes one type from another at the type level. This method ensures that types remain distinct without altering their runtime characteristics. Let's start by introducing the `BrandTypeId` symbol: ```ts twoslash const BrandTypeId: unique symbol = Symbol.for("effect/Brand") type ProductId = number & { readonly [BrandTypeId]: { readonly ProductId: "ProductId" // unique identifier for ProductId } } ``` This approach assigns a unique identifier as a brand to the `number` type, effectively differentiating `ProductId` from other numerical types. The use of a symbol ensures that the branding field does not conflict with any existing properties of the `number` type. Attempting to use a `UserId` in place of a `ProductId` now results in an error: **Example** (Enforcing Type Safety with Branded Types) ```ts twoslash const BrandTypeId: unique symbol = Symbol.for("effect/Brand") type ProductId = number & { readonly [BrandTypeId]: { readonly ProductId: "ProductId" } } const getProductById = (id: ProductId) => { // Logic to retrieve product } type UserId = number const id: UserId = 1 // @ts-expect-error getProductById(id) /* Argument of type 'number' is not assignable to parameter of type 'ProductId'. Type 'number' is not assignable to type '{ readonly [BrandTypeId]: { readonly ProductId: "ProductId"; }; }'.ts(2345) */ ``` The error message clearly states that a `number` cannot be used in place of a `ProductId`. TypeScript won't let us pass an instance of `number` to the function accepting `ProductId` because it's missing the brand field. Let's add branding to `UserId` as well: **Example** (Branding UserId and ProductId) ```ts twoslash const BrandTypeId: unique symbol = Symbol.for("effect/Brand") type ProductId = number & { readonly [BrandTypeId]: { readonly ProductId: "ProductId" // unique identifier for ProductId } } const getProductById = (id: ProductId) => { // Logic to retrieve product } type UserId = number & { readonly [BrandTypeId]: { readonly UserId: "UserId" // unique identifier for UserId } } declare const id: UserId // @ts-expect-error getProductById(id) /* Argument of type 'UserId' is not assignable to parameter of type 'ProductId'. Type 'UserId' is not assignable to type '{ readonly [BrandTypeId]: { readonly ProductId: "ProductId"; }; }'. Types of property '[BrandTypeId]' are incompatible. Property 'ProductId' is missing in type '{ readonly UserId: "UserId"; }' but required in type '{ readonly ProductId: "ProductId"; }'.ts(2345) */ ``` The error indicates that while both types use branding, the unique values associated with the branding fields (`"ProductId"` and `"UserId"`) ensure they remain distinct and non-interchangeable. ## Generalizing Branded Types To enhance the versatility and reusability of branded types, they can be generalized using a standardized approach: ```ts twoslash const BrandTypeId: unique symbol = Symbol.for("effect/Brand") // Create a generic Brand interface using a unique identifier interface Brand { readonly [BrandTypeId]: { readonly [id in ID]: ID } } // Define a ProductId type branded with a unique identifier type ProductId = number & Brand<"ProductId"> // Define a UserId type branded similarly type UserId = number & Brand<"UserId"> ``` This design allows any type to be branded using a unique identifier, either a string or symbol. Here's how you can utilize the `Brand` interface, which is readily available from the Brand module, eliminating the need to craft your own implementation: **Example** (Using the Brand Interface from the Brand Module) ```ts import { Brand } from "effect" // Define a ProductId type branded with a unique identifier type ProductId = number & Brand.Brand<"ProductId"> // Define a UserId type branded similarly type UserId = number & Brand.Brand<"UserId"> ``` However, creating instances of these types directly leads to an error because the type system expects the brand structure: **Example** (Direct Assignment Error) ```ts twoslash const BrandTypeId: unique symbol = Symbol.for("effect/Brand") interface Brand { readonly [BrandTypeId]: { readonly [k in K]: K } } type ProductId = number & Brand<"ProductId"> // @ts-expect-error const id: ProductId = 1 /* Type 'number' is not assignable to type 'ProductId'. Type 'number' is not assignable to type 'Brand<"ProductId">'.ts(2322) */ ``` You cannot directly assign a `number` to `ProductId`. The Brand module provides utilities to correctly construct values of branded types. ## Constructing Branded Types The Brand module provides two main functions for creating branded types: `nominal` and `refined`. ### nominal The `Brand.nominal` function is designed for defining branded types that do not require runtime validations. It simply adds a type tag to the underlying type, allowing us to distinguish between values of the same type but with different meanings. Nominal branded types are useful when we only want to create distinct types for clarity and code organization purposes. **Example** (Defining Distinct Identifiers with Nominal Branding) ```ts twoslash import { Brand } from "effect" // Define UserId as a branded number type UserId = number & Brand.Brand<"UserId"> // Constructor for UserId const UserId = Brand.nominal() const getUserById = (id: UserId) => { // Logic to retrieve user } // Define ProductId as a branded number type ProductId = number & Brand.Brand<"ProductId"> // Constructor for ProductId const ProductId = Brand.nominal() const getProductById = (id: ProductId) => { // Logic to retrieve product } ``` Attempting to assign a non-`ProductId` value will result in a compile-time error: **Example** (Type Safety with Branded Identifiers) ```ts twoslash import { Brand } from "effect" type UserId = number & Brand.Brand<"UserId"> const UserId = Brand.nominal() const getUserById = (id: UserId) => { // Logic to retrieve user } type ProductId = number & Brand.Brand<"ProductId"> const ProductId = Brand.nominal() const getProductById = (id: ProductId) => { // Logic to retrieve product } // Correct usage getProductById(ProductId(1)) // Incorrect, will result in an error // @ts-expect-error getProductById(1) /* Argument of type 'number' is not assignable to parameter of type 'ProductId'. Type 'number' is not assignable to type 'Brand<"ProductId">'.ts(2345) */ // Also incorrect, will result in an error // @ts-expect-error getProductById(UserId(1)) /* Argument of type 'UserId' is not assignable to parameter of type 'ProductId'. Type 'UserId' is not assignable to type 'Brand<"ProductId">'. Types of property '[BrandTypeId]' are incompatible. Property 'ProductId' is missing in type '{ readonly UserId: "UserId"; }' but required in type '{ readonly ProductId: "ProductId"; }'.ts(2345) */ ``` ### refined The `Brand.refined` function enables the creation of branded types that include data validation. It requires a refinement predicate to check the validity of input data against specific criteria. When the input data does not meet the criteria, the function uses `Brand.error` to generate a `BrandErrors` data type. This provides detailed information about why the validation failed. **Example** (Creating a Branded Type with Validation) ```ts twoslash import { Brand } from "effect" // Define a branded type 'Int' to represent integer values type Int = number & Brand.Brand<"Int"> // Define the constructor using 'refined' to enforce integer values const Int = Brand.refined( // Validation to ensure the value is an integer (n) => Number.isInteger(n), // Provide an error if validation fails (n) => Brand.error(`Expected ${n} to be an integer`) ) ``` **Example** (Using the `Int` Constructor) ```ts twoslash import { Brand } from "effect" type Int = number & Brand.Brand<"Int"> const Int = Brand.refined( // Check if the value is an integer (n) => Number.isInteger(n), // Error message if the value is not an integer (n) => Brand.error(`Expected ${n} to be an integer`) ) // Create a valid Int value const x: Int = Int(3) console.log(x) // Output: 3 // Attempt to create an Int with an invalid value const y: Int = Int(3.14) // throws [ { message: 'Expected 3.14 to be an integer' } ] ``` Attempting to assign a non-`Int` value will result in a compile-time error: **Example** (Compile-Time Error for Incorrect Assignments) ```ts twoslash import { Brand } from "effect" type Int = number & Brand.Brand<"Int"> const Int = Brand.refined( (n) => Number.isInteger(n), (n) => Brand.error(`Expected ${n} to be an integer`) ) // Correct usage const good: Int = Int(3) // Incorrect, will result in an error // @ts-expect-error const bad1: Int = 3 // Also incorrect, will result in an error // @ts-expect-error const bad2: Int = 3.14 ``` ## Combining Branded Types In some cases, you might need to combine multiple branded types. The Brand module provides the `Brand.all` API for this purpose: **Example** (Combining Multiple Branded Types) ```ts twoslash import { Brand } from "effect" type Int = number & Brand.Brand<"Int"> const Int = Brand.refined( (n) => Number.isInteger(n), (n) => Brand.error(`Expected ${n} to be an integer`) ) type Positive = number & Brand.Brand<"Positive"> const Positive = Brand.refined( (n) => n > 0, (n) => Brand.error(`Expected ${n} to be positive`) ) // Combine the Int and Positive constructors // into a new branded constructor PositiveInt const PositiveInt = Brand.all(Int, Positive) // Extract the branded type from the PositiveInt constructor type PositiveInt = Brand.Brand.FromConstructor // Usage example // Valid positive integer const good: PositiveInt = PositiveInt(10) // throws [ { message: 'Expected -5 to be positive' } ] const bad1: PositiveInt = PositiveInt(-5) // throws [ { message: 'Expected 3.14 to be an integer' } ] const bad2: PositiveInt = PositiveInt(3.14) ``` # [Simplifying Excessive Nesting](https://effect.website/docs/code-style/do/) ## Overview import { Steps } from "@astrojs/starlight/components" Suppose you want to create a custom function `elapsed` that prints the elapsed time taken by an effect to execute. ## Using plain pipe Initially, you may come up with code that uses the standard `pipe` [method](/docs/getting-started/building-pipelines/#the-pipe-method), but this approach can lead to excessive nesting and result in verbose and hard-to-read code: **Example** (Measuring Elapsed Time with `pipe`) ```ts twoslash import { Effect, Console } from "effect" // Get the current timestamp const now = Effect.sync(() => new Date().getTime()) // Prints the elapsed time occurred to `self` to execute const elapsed = ( self: Effect.Effect ): Effect.Effect => now.pipe( Effect.andThen((startMillis) => self.pipe( Effect.andThen((result) => now.pipe( Effect.andThen((endMillis) => { // Calculate the elapsed time in milliseconds const elapsed = endMillis - startMillis // Log the elapsed time return Console.log(`Elapsed: ${elapsed}`).pipe( Effect.map(() => result) ) }) ) ) ) ) ) // Simulates a successful computation with a delay of 200 milliseconds const task = Effect.succeed("some task").pipe(Effect.delay("200 millis")) const program = elapsed(task) Effect.runPromise(program).then(console.log) /* Output: Elapsed: 204 some task */ ``` To address this issue and make the code more manageable, there is a solution: the "do simulation." ## Using the "do simulation" The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like `Effect.bind` and `Effect.let`. Here's how the do simulation works: 1. Start the do simulation using the `Effect.Do` value: ```ts showLineNumbers=false const program = Effect.Do.pipe(/* ... rest of the code */) ``` 2. Within the do simulation scope, you can use the `Effect.bind` function to define variables and bind them to `Effect` values: ```ts showLineNumbers=false Effect.bind("variableName", (scope) => effectValue) ``` - `variableName` is the name you choose for the variable you want to define. It must be unique within the scope. - `effectValue` is the `Effect` value that you want to bind to the variable. It can be the result of a function call or any other valid `Effect` value. 3. You can accumulate multiple `Effect.bind` statements to define multiple variables within the scope: ```ts showLineNumbers=false Effect.bind("variable1", () => effectValue1), Effect.bind("variable2", ({ variable1 }) => effectValue2), // ... additional bind statements ``` 4. Inside the do simulation scope, you can also use the `Effect.let` function to define variables and bind them to simple values: ```ts showLineNumbers=false Effect.let("variableName", (scope) => simpleValue) ``` - `variableName` is the name you give to the variable. Like before, it must be unique within the scope. - `simpleValue` is the value you want to assign to the variable. It can be a simple value like a `number`, `string`, or `boolean`. 5. Regular Effect functions like `Effect.andThen`, `Effect.flatMap`, `Effect.tap`, and `Effect.map` can still be used within the do simulation. These functions will receive the accumulated variables as arguments within the scope: ```ts showLineNumbers=false Effect.andThen(({ variable1, variable2 }) => { // Perform operations using variable1 and variable2 // Return an `Effect` value as the result }) ``` With the do simulation, you can rewrite the `elapsed` function like this: **Example** (Using Do Simulation to Measure Elapsed Time) ```ts twoslash import { Effect, Console } from "effect" // Get the current timestamp const now = Effect.sync(() => new Date().getTime()) const elapsed = ( self: Effect.Effect ): Effect.Effect => Effect.Do.pipe( Effect.bind("startMillis", () => now), Effect.bind("result", () => self), Effect.bind("endMillis", () => now), Effect.let( "elapsed", // Calculate the elapsed time in milliseconds ({ startMillis, endMillis }) => endMillis - startMillis ), // Log the elapsed time Effect.tap(({ elapsed }) => Console.log(`Elapsed: ${elapsed}`)), Effect.map(({ result }) => result) ) // Simulates a successful computation with a delay of 200 milliseconds const task = Effect.succeed("some task").pipe(Effect.delay("200 millis")) const program = elapsed(task) Effect.runPromise(program).then(console.log) /* Output: Elapsed: 204 some task */ ``` ## Using Effect.gen The most concise and convenient solution is to use [Effect.gen](/docs/getting-started/using-generators/), which allows you to work with [generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator) when dealing with effects. This approach leverages the native scope provided by the generator syntax, avoiding excessive nesting and leading to more concise code. **Example** (Using Effect.gen to Measure Elapsed Time) ```ts twoslash import { Effect } from "effect" // Get the current timestamp const now = Effect.sync(() => new Date().getTime()) // Prints the elapsed time occurred to `self` to execute const elapsed = ( self: Effect.Effect ): Effect.Effect => Effect.gen(function* () { const startMillis = yield* now const result = yield* self const endMillis = yield* now // Calculate the elapsed time in milliseconds const elapsed = endMillis - startMillis // Log the elapsed time console.log(`Elapsed: ${elapsed}`) return result }) // Simulates a successful computation with a delay of 200 milliseconds const task = Effect.succeed("some task").pipe(Effect.delay("200 millis")) const program = elapsed(task) Effect.runPromise(program).then(console.log) /* Output: Elapsed: 204 some task */ ``` Within the generator, we use `yield*` to invoke effects and bind their results to variables. This eliminates the nesting and provides a more readable and sequential code structure. The generator style in Effect uses a more linear and sequential flow of execution, resembling traditional imperative programming languages. This makes the code easier to read and understand, especially for developers who are more familiar with imperative programming paradigms. # [Dual APIs](https://effect.website/docs/code-style/dual/) ## Overview import { Aside } from "@astrojs/starlight/components" When you're working with APIs in the Effect ecosystem, you may come across two different ways to use the same API. These two ways are called the "data-last" and "data-first" variants. When an API supports both variants, we call them "dual" APIs. Here's an illustration of these two variants using `Effect.map`. ## Effect.map as a dual API The `Effect.map` function is defined with two TypeScript overloads. The terms "data-last" and "data-first" refer to the position of the `self` argument (also known as the "data") in the signatures of the two overloads: ```ts showLineNumbers=false declare const map: { // ┌─── data-last // ▼ (f: (a: A) => B): (self: Effect) => Effect // ┌─── data-first // ▼ (self: Effect, f: (a: A) => B): Effect } ``` ### data-last In the first overload, the `self` argument comes **last**: ```ts showLineNumbers=false "self" declare const map: ( f: (a: A) => B ) => (self: Effect) => Effect ``` This version is commonly used with the `pipe` function. You start by passing the `Effect` as the initial argument to `pipe` and then chain transformations like `Effect.map`: **Example** (Using data-last with `pipe`) ```ts showLineNumbers=false const mappedEffect = pipe(effect, Effect.map(func)) ``` This style is helpful when chaining multiple transformations, making the code easier to follow in a pipeline format: ```ts showLineNumbers=false pipe(effect, Effect.map(func1), Effect.map(func2), ...) ``` ### data-first In the second overload, the `self` argument comes **first**: ```ts showLineNumbers=false "self" declare const map: ( self: Effect, f: (a: A) => B ) => Effect ``` This form doesn't require `pipe`. Instead, you provide the `Effect` directly as the first argument: **Example** (Using data-first without `pipe`) ```ts showLineNumbers=false const mappedEffect = Effect.map(effect, func) ``` This version works well when you only need to perform a single operation on the `Effect`. # [Guidelines](https://effect.website/docs/code-style/guidelines/) ## Overview import { Aside } from "@astrojs/starlight/components" ## Using runMain In Effect, `runMain` is the primary entry point for executing an Effect application on Node.js. **Example** (Running an Effect Application with Graceful Teardown) ```ts import { Effect, Console, Schedule, pipe } from "effect" import { NodeRuntime } from "@effect/platform-node" const program = pipe( Effect.addFinalizer(() => Console.log("Application is about to exit!")), Effect.andThen(Console.log("Application started!")), Effect.andThen( Effect.repeat(Console.log("still alive..."), { schedule: Schedule.spaced("1 second") }) ), Effect.scoped ) // No graceful teardown on CTRL+C // Effect.runPromise(program) // Use NodeRuntime.runMain for graceful teardown on CTRL+C NodeRuntime.runMain(program) /* Output: Application started! still alive... still alive... still alive... still alive... ^C <-- CTRL+C Application is about to exit! */ ``` The `runMain` function handles finding and interrupting all fibers. Internally, it observes the fiber and listens for `sigint` signals, ensuring a graceful shutdown of the application when interrupted (e.g., using CTRL+C). ### Versions for Different Platforms Effect provides versions of `runMain` tailored for different platforms: | Platform | Runtime Version | Import Path | | -------- | ------------------------ | -------------------------- | | Node.js | `NodeRuntime.runMain` | `@effect/platform-node` | | Bun | `BunRuntime.runMain` | `@effect/platform-bun` | | Browser | `BrowserRuntime.runMain` | `@effect/platform-browser` | ## Avoid Tacit Usage Avoid using tacit (point-free) function calls, such as `Effect.map(fn)`, or using `flow` from the `effect/Function` module. In Effect, it's generally safer to write functions explicitly: ```ts showLineNumbers=false Effect.map((x) => fn(x)) ``` rather than in a point-free style: ```ts showLineNumbers=false Effect.map(fn) ``` While tacit functions may be appealing for their brevity, they can introduce a number of problems: - Using tacit functions, particularly when dealing with optional parameters, can be unsafe. For example, if a function has overloads, writing it in a tacit style may erase all generics, resulting in bugs. Check out this X thread for more details: [link to thread](https://twitter.com/MichaelArnaldi/status/1670715270845935616). - Tacit usage can also compromise TypeScript's ability to infer types, potentially causing unexpected errors. This isn't just a matter of style but a way to avoid subtle mistakes that can arise from type inference issues. - Additionally, stack traces might not be as clear when tacit usage is employed. Avoiding tacit usage is a simple precaution that makes your code more reliable. # [Pattern Matching](https://effect.website/docs/code-style/pattern-matching/) ## Overview import { Aside } from "@astrojs/starlight/components" Pattern matching is a method that allows developers to handle intricate conditions within a single, concise expression. It simplifies code, making it more concise and easier to understand. Additionally, it includes a process called exhaustiveness checking, which helps to ensure that no possible case has been overlooked. Originating from functional programming languages, pattern matching stands as a powerful technique for code branching. It often offers a more potent and less verbose solution compared to imperative alternatives such as if/else or switch statements, particularly when dealing with complex conditions. Although not yet a native feature in JavaScript, there's an ongoing [tc39 proposal](https://github.com/tc39/proposal-pattern-matching) in its early stages to introduce pattern matching to JavaScript. However, this proposal is at stage 1 and might take several years to be implemented. Nonetheless, developers can implement pattern matching in their codebase. The `effect/Match` module provides a reliable, type-safe pattern matching implementation that is available for immediate use. **Example** (Handling Different Data Types with Pattern Matching) ```ts twoslash import { Match } from "effect" // Simulated dynamic input that can be a string or a number const input: string | number = "some input" // ┌─── string // ▼ const result = Match.value(input).pipe( // Match if the value is a number Match.when(Match.number, (n) => `number: ${n}`), // Match if the value is a string Match.when(Match.string, (s) => `string: ${s}`), // Ensure all possible cases are covered Match.exhaustive ) console.log(result) // Output: "string: some input" ``` ## How Pattern Matching Works Pattern matching follows a structured process: 1. **Creating a matcher**. Define a `Matcher` that operates on either a specific [type](#matching-by-type) or [value](#matching-by-value). 2. **Defining patterns**. Use combinators such as `Match.when`, `Match.not`, and `Match.tag` to specify matching conditions. 3. **Completing the match**. Apply a finalizer such as `Match.exhaustive`, `Match.orElse`, or `Match.option` to determine how unmatched cases should be handled. ## Creating a matcher You can create a `Matcher` using either: - `Match.type()`: Matches against a specific type. - `Match.value(value)`: Matches against a specific value. ### Matching by Type The `Match.type` constructor defines a `Matcher` that operates on a specific type. Once created, you can use patterns like `Match.when` to define conditions for handling different cases. **Example** (Matching Numbers and Strings) ```ts twoslash import { Match } from "effect" // Create a matcher for values that are either strings or numbers // // ┌─── (u: string | number) => string // ▼ const match = Match.type().pipe( // Match when the value is a number Match.when(Match.number, (n) => `number: ${n}`), // Match when the value is a string Match.when(Match.string, (s) => `string: ${s}`), // Ensure all possible cases are handled Match.exhaustive ) console.log(match(0)) // Output: "number: 0" console.log(match("hello")) // Output: "string: hello" ``` ### Matching by Value Instead of creating a matcher for a type, you can define one directly from a specific value using `Match.value`. **Example** (Matching an Object by Property) ```ts twoslash import { Match } from "effect" const input = { name: "John", age: 30 } // Create a matcher for the specific object const result = Match.value(input).pipe( // Match when the 'name' property is "John" Match.when( { name: "John" }, (user) => `${user.name} is ${user.age} years old` ), // Provide a fallback if no match is found Match.orElse(() => "Oh, not John") ) console.log(result) // Output: "John is 30 years old" ``` ### Enforcing a Return Type You can use `Match.withReturnType()` to ensure that all branches return a specific type. **Example** (Validating Return Type Consistency) This example enforces that every matching branch returns a `string`. ```ts twoslash import { Match } from "effect" const match = Match.type<{ a: number } | { b: string }>().pipe( // Ensure all branches return a string Match.withReturnType(), // ❌ Type error: 'number' is not assignable to type 'string' // @ts-expect-error Match.when({ a: Match.number }, (_) => _.a), // ✅ Correct: returns a string Match.when({ b: Match.string }, (_) => _.b), Match.exhaustive ) ``` ## Defining patterns ### when The `Match.when` function allows you to define conditions for matching values. It supports both direct value comparisons and predicate functions. **Example** (Matching with Values and Predicates) ```ts twoslash import { Match } from "effect" // Create a matcher for objects with an "age" property const match = Match.type<{ age: number }>().pipe( // Match when age is greater than 18 Match.when({ age: (age) => age > 18 }, (user) => `Age: ${user.age}`), // Match when age is exactly 18 Match.when({ age: 18 }, () => "You can vote"), // Fallback case for all other ages Match.orElse((user) => `${user.age} is too young`) ) console.log(match({ age: 20 })) // Output: "Age: 20" console.log(match({ age: 18 })) // Output: "You can vote" console.log(match({ age: 4 })) // Output: "4 is too young" ``` ### not The `Match.not` function allows you to exclude specific values while matching all others. **Example** (Ignoring a Specific Value) ```ts twoslash import { Match } from "effect" // Create a matcher for string or number values const match = Match.type().pipe( // Match any value except "hi", returning "ok" Match.not("hi", () => "ok"), // Fallback case for when the value is "hi" Match.orElse(() => "fallback") ) console.log(match("hello")) // Output: "ok" console.log(match("hi")) // Output: "fallback" ``` ### tag The `Match.tag` function allows pattern matching based on the `_tag` field in a [Discriminated Union](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#discriminated-unions). You can specify multiple tags to match within a single pattern. **Example** (Matching a Discriminated Union by Tag) ```ts twoslash import { Match } from "effect" type Event = | { readonly _tag: "fetch" } | { readonly _tag: "success"; readonly data: string } | { readonly _tag: "error"; readonly error: Error } | { readonly _tag: "cancel" } // Create a Matcher for Either const match = Match.type().pipe( // Match either "fetch" or "success" Match.tag("fetch", "success", () => `Ok!`), // Match "error" and extract the error message Match.tag("error", (event) => `Error: ${event.error.message}`), // Match "cancel" Match.tag("cancel", () => "Cancelled"), Match.exhaustive ) console.log(match({ _tag: "success", data: "Hello" })) // Output: "Ok!" console.log(match({ _tag: "error", error: new Error("Oops!") })) // Output: "Error: Oops!" ``` ### Built-in Predicates The `Match` module provides built-in predicates for common types, such as `Match.number`, `Match.string`, and `Match.boolean`. These predicates simplify the process of matching against primitive types. **Example** (Using Built-in Predicates for Property Keys) ```ts twoslash import { Match } from "effect" const matchPropertyKey = Match.type().pipe( // Match when the value is a number Match.when(Match.number, (n) => `Key is a number: ${n}`), // Match when the value is a string Match.when(Match.string, (s) => `Key is a string: ${s}`), // Match when the value is a symbol Match.when(Match.symbol, (s) => `Key is a symbol: ${String(s)}`), // Ensure all possible cases are handled Match.exhaustive ) console.log(matchPropertyKey(42)) // Output: "Key is a number: 42" console.log(matchPropertyKey("username")) // Output: "Key is a string: username" console.log(matchPropertyKey(Symbol("id"))) // Output: "Key is a symbol: Symbol(id)" ``` | Predicate | Description | | ------------------------- | ----------------------------------------------------------------------------- | | `Match.string` | Matches values of type `string`. | | `Match.nonEmptyString` | Matches non-empty strings. | | `Match.number` | Matches values of type `number`. | | `Match.boolean` | Matches values of type `boolean`. | | `Match.bigint` | Matches values of type `bigint`. | | `Match.symbol` | Matches values of type `symbol`. | | `Match.date` | Matches values that are instances of `Date`. | | `Match.record` | Matches objects where keys are `string` or `symbol` and values are `unknown`. | | `Match.null` | Matches the value `null`. | | `Match.undefined` | Matches the value `undefined`. | | `Match.defined` | Matches any defined (non-null and non-undefined) value. | | `Match.any` | Matches any value without restrictions. | | `Match.is(...values)` | Matches a specific set of literal values (e.g., `Match.is("a", 42, true)`). | | `Match.instanceOf(Class)` | Matches instances of a given class. | ## Completing the match ### exhaustive The `Match.exhaustive` method finalizes the pattern matching process by ensuring that all possible cases are accounted for. If any case is missing, TypeScript will produce a type error. This is particularly useful when working with unions, as it helps prevent unintended gaps in pattern matching. **Example** (Ensuring All Cases Are Covered) ```ts twoslash import { Match } from "effect" // Create a matcher for string or number values const match = Match.type().pipe( // Match when the value is a number Match.when(Match.number, (n) => `number: ${n}`), // Mark the match as exhaustive, ensuring all cases are handled // TypeScript will throw an error if any case is missing // @ts-expect-error Type 'string' is not assignable to type 'never' Match.exhaustive ) ``` ### orElse The `Match.orElse` method defines a fallback value to return when no other patterns match. This ensures that the matcher always produces a valid result. **Example** (Providing a Default Value When No Patterns Match) ```ts twoslash import { Match } from "effect" // Create a matcher for string or number values const match = Match.type().pipe( // Match when the value is "a" Match.when("a", () => "ok"), // Fallback when no patterns match Match.orElse(() => "fallback") ) console.log(match("a")) // Output: "ok" console.log(match("b")) // Output: "fallback" ``` ### option `Match.option` wraps the match result in an [Option](/docs/data-types/option/). If a match is found, it returns `Some(value)`, otherwise, it returns `None`. **Example** (Extracting a User Role with Option) ```ts twoslash import { Match } from "effect" type User = { readonly role: "admin" | "editor" | "viewer" } // Create a matcher to extract user roles const getRole = Match.type().pipe( Match.when({ role: "admin" }, () => "Has full access"), Match.when({ role: "editor" }, () => "Can edit content"), Match.option // Wrap the result in an Option ) console.log(getRole({ role: "admin" })) // Output: { _id: 'Option', _tag: 'Some', value: 'Has full access' } console.log(getRole({ role: "viewer" })) // Output: { _id: 'Option', _tag: 'None' } ``` ### either The `Match.either` method wraps the result in an [Either](/docs/data-types/either/), providing a structured way to distinguish between matched and unmatched cases. If a match is found, it returns `Right(value)`, otherwise, it returns `Left(no match)`. **Example** (Extracting a User Role with Either) ```ts twoslash import { Match } from "effect" type User = { readonly role: "admin" | "editor" | "viewer" } // Create a matcher to extract user roles const getRole = Match.type().pipe( Match.when({ role: "admin" }, () => "Has full access"), Match.when({ role: "editor" }, () => "Can edit content"), Match.either // Wrap the result in an Either ) console.log(getRole({ role: "admin" })) // Output: { _id: 'Either', _tag: 'Right', right: 'Has full access' } console.log(getRole({ role: "viewer" })) // Output: { _id: 'Either', _tag: 'Left', left: { role: 'viewer' } } ``` # [Basic Concurrency](https://effect.website/docs/concurrency/basic-concurrency/) ## Overview import { Aside } from "@astrojs/starlight/components" ## Concurrency Options Effect provides options to manage how effects are executed, particularly focusing on controlling how many effects run concurrently. ```ts showLineNumbers=false type Options = { readonly concurrency?: Concurrency } ``` The `concurrency` option is used to determine the level of concurrency, with the following values: ```ts showLineNumbers=false type Concurrency = number | "unbounded" | "inherit" ``` Let's explore each configuration in detail. ### Sequential Execution (Default) By default, if you don't specify any concurrency option, effects will run sequentially, one after the other. This means each effect starts only after the previous one completes. **Example** (Sequential Execution) ```ts twoslash import { Effect, Duration } from "effect" // Helper function to simulate a task with a delay const makeTask = (n: number, delay: Duration.DurationInput) => Effect.promise( () => new Promise((resolve) => { console.log(`start task${n}`) // Logs when the task starts setTimeout(() => { console.log(`task${n} done`) // Logs when the task finishes resolve() }, Duration.toMillis(delay)) }) ) const task1 = makeTask(1, "200 millis") const task2 = makeTask(2, "100 millis") const sequential = Effect.all([task1, task2]) Effect.runPromise(sequential) /* Output: start task1 task1 done start task2 <-- task2 starts only after task1 completes task2 done */ ``` ### Numbered Concurrency You can control how many effects run concurrently by setting a `number` for `concurrency`. For example, `concurrency: 2` allows up to two effects to run at the same time. **Example** (Limiting to 2 Concurrent Tasks) ```ts twoslash import { Effect, Duration } from "effect" // Helper function to simulate a task with a delay const makeTask = (n: number, delay: Duration.DurationInput) => Effect.promise( () => new Promise((resolve) => { console.log(`start task${n}`) // Logs when the task starts setTimeout(() => { console.log(`task${n} done`) // Logs when the task finishes resolve() }, Duration.toMillis(delay)) }) ) const task1 = makeTask(1, "200 millis") const task2 = makeTask(2, "100 millis") const task3 = makeTask(3, "210 millis") const task4 = makeTask(4, "110 millis") const task5 = makeTask(5, "150 millis") const numbered = Effect.all([task1, task2, task3, task4, task5], { concurrency: 2 }) Effect.runPromise(numbered) /* Output: start task1 start task2 <-- active tasks: task1, task2 task2 done start task3 <-- active tasks: task1, task3 task1 done start task4 <-- active tasks: task3, task4 task4 done start task5 <-- active tasks: task3, task5 task3 done task5 done */ ``` ### Unbounded Concurrency When `concurrency: "unbounded"` is used, there's no limit to the number of effects running concurrently. **Example** (Unbounded Concurrency) ```ts twoslash import { Effect, Duration } from "effect" // Helper function to simulate a task with a delay const makeTask = (n: number, delay: Duration.DurationInput) => Effect.promise( () => new Promise((resolve) => { console.log(`start task${n}`) // Logs when the task starts setTimeout(() => { console.log(`task${n} done`) // Logs when the task finishes resolve() }, Duration.toMillis(delay)) }) ) const task1 = makeTask(1, "200 millis") const task2 = makeTask(2, "100 millis") const task3 = makeTask(3, "210 millis") const task4 = makeTask(4, "110 millis") const task5 = makeTask(5, "150 millis") const unbounded = Effect.all([task1, task2, task3, task4, task5], { concurrency: "unbounded" }) Effect.runPromise(unbounded) /* Output: start task1 start task2 start task3 start task4 start task5 task2 done task4 done task5 done task1 done task3 done */ ``` ### Inherit Concurrency When using `concurrency: "inherit"`, the concurrency level is inherited from the surrounding context. This context can be set using `Effect.withConcurrency(number | "unbounded")`. If no context is provided, the default is `"unbounded"`. **Example** (Inheriting Concurrency from Context) ```ts twoslash import { Effect, Duration } from "effect" // Helper function to simulate a task with a delay const makeTask = (n: number, delay: Duration.DurationInput) => Effect.promise( () => new Promise((resolve) => { console.log(`start task${n}`) // Logs when the task starts setTimeout(() => { console.log(`task${n} done`) // Logs when the task finishes resolve() }, Duration.toMillis(delay)) }) ) const task1 = makeTask(1, "200 millis") const task2 = makeTask(2, "100 millis") const task3 = makeTask(3, "210 millis") const task4 = makeTask(4, "110 millis") const task5 = makeTask(5, "150 millis") // Running all tasks with concurrency: "inherit", // which defaults to "unbounded" const inherit = Effect.all([task1, task2, task3, task4, task5], { concurrency: "inherit" }) Effect.runPromise(inherit) /* Output: start task1 start task2 start task3 start task4 start task5 task2 done task4 done task5 done task1 done task3 done */ ``` If you use `Effect.withConcurrency`, the concurrency configuration will adjust to the specified option. **Example** (Setting Concurrency Option) ```ts twoslash import { Effect, Duration } from "effect" // Helper function to simulate a task with a delay const makeTask = (n: number, delay: Duration.DurationInput) => Effect.promise( () => new Promise((resolve) => { console.log(`start task${n}`) // Logs when the task starts setTimeout(() => { console.log(`task${n} done`) // Logs when the task finishes resolve() }, Duration.toMillis(delay)) }) ) const task1 = makeTask(1, "200 millis") const task2 = makeTask(2, "100 millis") const task3 = makeTask(3, "210 millis") const task4 = makeTask(4, "110 millis") const task5 = makeTask(5, "150 millis") // Running tasks with concurrency: "inherit", // which will inherit the surrounding context const inherit = Effect.all([task1, task2, task3, task4, task5], { concurrency: "inherit" }) // Setting a concurrency limit of 2 const withConcurrency = inherit.pipe(Effect.withConcurrency(2)) Effect.runPromise(withConcurrency) /* Output: start task1 start task2 <-- active tasks: task1, task2 task2 done start task3 <-- active tasks: task1, task3 task1 done start task4 <-- active tasks: task3, task4 task4 done start task5 <-- active tasks: task3, task5 task3 done task5 done */ ``` ## Interruptions All effects in Effect are executed by [fibers](/docs/concurrency/fibers/). If you didn't create the fiber yourself, it was created by an operation you're using (if it's concurrent) or by the Effect [runtime](/docs/runtime/) system. A fiber is created any time an effect is run. When running effects concurrently, a fiber is created for each concurrent effect. To summarize: - An `Effect` is a higher-level concept that describes an effectful computation. It is lazy and immutable, meaning it represents a computation that may produce a value or fail but does not immediately execute. - A fiber, on the other hand, represents the running execution of an `Effect`. It can be interrupted or awaited to retrieve its result. Think of it as a way to control and interact with the ongoing computation. Fibers can be interrupted in various ways. Let's explore some of these scenarios and see examples of how to interrupt fibers in Effect. ### interrupt A fiber can be interrupted using the `Effect.interrupt` effect on that particular fiber. This effect models the explicit interruption of the fiber in which it runs. When executed, it causes the fiber to stop its operation immediately, capturing the interruption details such as the fiber's ID and its start time. The resulting interruption can be observed in the [Exit](/docs/data-types/exit/) type if the effect is run with functions like [runPromiseExit](/docs/getting-started/running-effects/#runpromiseexit). **Example** (Without Interruption) In this case, the program runs without any interruption, logging the start and completion of the task. ```ts twoslash import { Effect } from "effect" const program = Effect.gen(function* () { console.log("start") yield* Effect.sleep("2 seconds") console.log("done") return "some result" }) Effect.runPromiseExit(program).then(console.log) /* Output: start done { _id: 'Exit', _tag: 'Success', value: 'some result' } */ ``` **Example** (With Interruption) Here, the fiber is interrupted after the log `"start"` but before the `"done"` log. The `Effect.interrupt` stops the fiber, and it never reaches the final log. ```ts {6} twoslash import { Effect } from "effect" const program = Effect.gen(function* () { console.log("start") yield* Effect.sleep("2 seconds") yield* Effect.interrupt console.log("done") return "some result" }) Effect.runPromiseExit(program).then(console.log) /* Output: start { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Interrupt', fiberId: { _id: 'FiberId', _tag: 'Runtime', id: 0, startTimeMillis: ... } } } */ ``` ### onInterrupt Registers a cleanup effect to run when an effect is interrupted. This function allows you to specify an effect to run when the fiber is interrupted. This effect will be executed when the fiber is interrupted, allowing you to perform cleanup or other actions. **Example** (Running a Cleanup Action on Interruption) In this example, we set up a handler that logs "Cleanup completed" whenever the fiber is interrupted. We then show three cases: a successful effect, a failing effect, and an interrupted effect, demonstrating how the handler is triggered depending on how the effect ends. ```ts twoslash import { Console, Effect } from "effect" // This handler is executed when the fiber is interrupted const handler = Effect.onInterrupt((_fibers) => Console.log("Cleanup completed") ) const success = Console.log("Task completed").pipe( Effect.as("some result"), handler ) Effect.runFork(success) /* Output: Task completed */ const failure = Console.log("Task failed").pipe( Effect.andThen(Effect.fail("some error")), handler ) Effect.runFork(failure) /* Output: Task failed */ const interruption = Console.log("Task interrupted").pipe( Effect.andThen(Effect.interrupt), handler ) Effect.runFork(interruption) /* Output: Task interrupted Cleanup completed */ ``` ### Interruption of Concurrent Effects When running multiple effects concurrently, such as with `Effect.forEach`, if one of the effects is interrupted, it causes all concurrent effects to be interrupted as well. The resulting [cause](/docs/data-types/cause/) includes information about which fibers were interrupted. **Example** (Interrupting Concurrent Effects) ```ts twoslash import { Effect, Console } from "effect" const program = Effect.forEach( [1, 2, 3], (n) => Effect.gen(function* () { console.log(`start #${n}`) yield* Effect.sleep(`${n} seconds`) if (n > 1) { yield* Effect.interrupt } console.log(`done #${n}`) }).pipe(Effect.onInterrupt(() => Console.log(`interrupted #${n}`))), { concurrency: "unbounded" } ) Effect.runPromiseExit(program).then((exit) => console.log(JSON.stringify(exit, null, 2)) ) /* Output: start #1 start #2 start #3 done #1 interrupted #2 interrupted #3 { "_id": "Exit", "_tag": "Failure", "cause": { "_id": "Cause", "_tag": "Parallel", "left": { "_id": "Cause", "_tag": "Interrupt", "fiberId": { "_id": "FiberId", "_tag": "Runtime", "id": 3, "startTimeMillis": ... } }, "right": { "_id": "Cause", "_tag": "Sequential", "left": { "_id": "Cause", "_tag": "Empty" }, "right": { "_id": "Cause", "_tag": "Interrupt", "fiberId": { "_id": "FiberId", "_tag": "Runtime", "id": 0, "startTimeMillis": ... } } } } } */ ``` ## Racing ### race This function takes two effects and runs them concurrently. The first effect that successfully completes will determine the result of the race, and the other effect will be interrupted. If neither effect succeeds, the function will fail with a [cause](/docs/data-types/cause/) containing all the errors. This is useful when you want to run two effects concurrently, but only care about the first one to succeed. It is commonly used in cases like timeouts, retries, or when you want to optimize for the faster response without worrying about the other effect. **Example** (Both Tasks Succeed) ```ts twoslash import { Effect, Console } from "effect" const task1 = Effect.succeed("task1").pipe( Effect.delay("200 millis"), Effect.tap(Console.log("task1 done")), Effect.onInterrupt(() => Console.log("task1 interrupted")) ) const task2 = Effect.succeed("task2").pipe( Effect.delay("100 millis"), Effect.tap(Console.log("task2 done")), Effect.onInterrupt(() => Console.log("task2 interrupted")) ) const program = Effect.race(task1, task2) Effect.runFork(program) /* Output: task1 done task2 interrupted */ ``` **Example** (One Task Fails, One Succeeds) ```ts twoslash import { Effect, Console } from "effect" const task1 = Effect.fail("task1").pipe( Effect.delay("100 millis"), Effect.tap(Console.log("task1 done")), Effect.onInterrupt(() => Console.log("task1 interrupted")) ) const task2 = Effect.succeed("task2").pipe( Effect.delay("200 millis"), Effect.tap(Console.log("task2 done")), Effect.onInterrupt(() => Console.log("task2 interrupted")) ) const program = Effect.race(task1, task2) Effect.runFork(program) /* Output: task2 done */ ``` **Example** (Both Tasks Fail) ```ts twoslash import { Effect, Console } from "effect" const task1 = Effect.fail("task1").pipe( Effect.delay("100 millis"), Effect.tap(Console.log("task1 done")), Effect.onInterrupt(() => Console.log("task1 interrupted")) ) const task2 = Effect.fail("task2").pipe( Effect.delay("200 millis"), Effect.tap(Console.log("task2 done")), Effect.onInterrupt(() => Console.log("task2 interrupted")) ) const program = Effect.race(task1, task2) Effect.runPromiseExit(program).then(console.log) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Parallel', left: { _id: 'Cause', _tag: 'Fail', failure: 'task1' }, right: { _id: 'Cause', _tag: 'Fail', failure: 'task2' } } } */ ``` If you want to handle the result of whichever task completes first, whether it succeeds or fails, you can use the `Effect.either` function. This function wraps the result in an [Either](/docs/data-types/either/) type, allowing you to see if the result was a success (`Right`) or a failure (`Left`): **Example** (Handling Success or Failure with Either) ```ts twoslash import { Effect, Console } from "effect" const task1 = Effect.fail("task1").pipe( Effect.delay("100 millis"), Effect.tap(Console.log("task1 done")), Effect.onInterrupt(() => Console.log("task1 interrupted")) ) const task2 = Effect.succeed("task2").pipe( Effect.delay("200 millis"), Effect.tap(Console.log("task2 done")), Effect.onInterrupt(() => Console.log("task2 interrupted")) ) // Run both tasks concurrently, wrapping the result // in Either to capture success or failure const program = Effect.race(Effect.either(task1), Effect.either(task2)) Effect.runPromise(program).then(console.log) /* Output: task2 interrupted { _id: 'Either', _tag: 'Left', left: 'task1' } */ ``` ### raceAll This function runs multiple effects concurrently and returns the result of the first one to succeed. If one effect succeeds, the others will be interrupted. If none of the effects succeed, the function will fail with the last error encountered. This is useful when you want to race multiple effects, but only care about the first one to succeed. It is commonly used in cases like timeouts, retries, or when you want to optimize for the faster response without worrying about the other effects. **Example** (All Tasks Succeed) ```ts twoslash import { Effect, Console } from "effect" const task1 = Effect.succeed("task1").pipe( Effect.delay("100 millis"), Effect.tap(Console.log("task1 done")), Effect.onInterrupt(() => Console.log("task1 interrupted")) ) const task2 = Effect.succeed("task2").pipe( Effect.delay("200 millis"), Effect.tap(Console.log("task2 done")), Effect.onInterrupt(() => Console.log("task2 interrupted")) ) const task3 = Effect.succeed("task3").pipe( Effect.delay("150 millis"), Effect.tap(Console.log("task3 done")), Effect.onInterrupt(() => Console.log("task3 interrupted")) ) const program = Effect.raceAll([task1, task2, task3]) Effect.runFork(program) /* Output: task1 done task2 interrupted task3 interrupted */ ``` **Example** (One Task Fails, Two Tasks Succeed) ```ts twoslash import { Effect, Console } from "effect" const task1 = Effect.fail("task1").pipe( Effect.delay("100 millis"), Effect.tap(Console.log("task1 done")), Effect.onInterrupt(() => Console.log("task1 interrupted")) ) const task2 = Effect.succeed("task2").pipe( Effect.delay("200 millis"), Effect.tap(Console.log("task2 done")), Effect.onInterrupt(() => Console.log("task2 interrupted")) ) const task3 = Effect.succeed("task3").pipe( Effect.delay("150 millis"), Effect.tap(Console.log("task3 done")), Effect.onInterrupt(() => Console.log("task3 interrupted")) ) const program = Effect.raceAll([task1, task2, task3]) Effect.runFork(program) /* Output: task3 done task2 interrupted */ ``` **Example** (All Tasks Fail) ```ts twoslash import { Effect, Console } from "effect" const task1 = Effect.fail("task1").pipe( Effect.delay("100 millis"), Effect.tap(Console.log("task1 done")), Effect.onInterrupt(() => Console.log("task1 interrupted")) ) const task2 = Effect.fail("task2").pipe( Effect.delay("200 millis"), Effect.tap(Console.log("task2 done")), Effect.onInterrupt(() => Console.log("task2 interrupted")) ) const task3 = Effect.fail("task3").pipe( Effect.delay("150 millis"), Effect.tap(Console.log("task3 done")), Effect.onInterrupt(() => Console.log("task3 interrupted")) ) const program = Effect.raceAll([task1, task2, task3]) Effect.runPromiseExit(program).then(console.log) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'task2' } } */ ``` ### raceFirst This function takes two effects and runs them concurrently, returning the result of the first one that completes, regardless of whether it succeeds or fails. This function is useful when you want to race two operations, and you want to proceed with whichever one finishes first, regardless of whether it succeeds or fails. **Example** (Both Tasks Succeed) ```ts twoslash import { Effect, Console } from "effect" const task1 = Effect.succeed("task1").pipe( Effect.delay("100 millis"), Effect.tap(Console.log("task1 done")), Effect.onInterrupt(() => Console.log("task1 interrupted").pipe(Effect.delay("100 millis")) ) ) const task2 = Effect.succeed("task2").pipe( Effect.delay("200 millis"), Effect.tap(Console.log("task2 done")), Effect.onInterrupt(() => Console.log("task2 interrupted").pipe(Effect.delay("100 millis")) ) ) const program = Effect.raceFirst(task1, task2).pipe( Effect.tap(Console.log("more work...")) ) Effect.runPromiseExit(program).then(console.log) /* Output: task1 done task2 interrupted more work... { _id: 'Exit', _tag: 'Success', value: 'task1' } */ ``` **Example** (One Task Fails, One Succeeds) ```ts twoslash import { Effect, Console } from "effect" const task1 = Effect.fail("task1").pipe( Effect.delay("100 millis"), Effect.tap(Console.log("task1 done")), Effect.onInterrupt(() => Console.log("task1 interrupted").pipe(Effect.delay("100 millis")) ) ) const task2 = Effect.succeed("task2").pipe( Effect.delay("200 millis"), Effect.tap(Console.log("task2 done")), Effect.onInterrupt(() => Console.log("task2 interrupted").pipe(Effect.delay("100 millis")) ) ) const program = Effect.raceFirst(task1, task2).pipe( Effect.tap(Console.log("more work...")) ) Effect.runPromiseExit(program).then(console.log) /* Output: task2 interrupted { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'task1' } } */ ``` #### Disconnecting Effects The `Effect.raceFirst` function safely interrupts the "loser" effect once the other completes, but it will not resume until the loser is cleanly terminated. If you want a quicker return, you can disconnect the interrupt signal for both effects. Instead of calling: ```ts showLineNumbers=false Effect.raceFirst(task1, task2) ``` You can use: ```ts showLineNumbers=false Effect.raceFirst(Effect.disconnect(task1), Effect.disconnect(task2)) ``` This allows both effects to complete independently while still terminating the losing effect in the background. **Example** (Using `Effect.disconnect` for Quicker Return) ```ts twoslash import { Effect, Console } from "effect" const task1 = Effect.succeed("task1").pipe( Effect.delay("100 millis"), Effect.tap(Console.log("task1 done")), Effect.onInterrupt(() => Console.log("task1 interrupted").pipe(Effect.delay("100 millis")) ) ) const task2 = Effect.succeed("task2").pipe( Effect.delay("200 millis"), Effect.tap(Console.log("task2 done")), Effect.onInterrupt(() => Console.log("task2 interrupted").pipe(Effect.delay("100 millis")) ) ) // Race the two tasks with disconnect to allow quicker return const program = Effect.raceFirst( Effect.disconnect(task1), Effect.disconnect(task2) ).pipe(Effect.tap(Console.log("more work..."))) Effect.runPromiseExit(program).then(console.log) /* Output: task1 done more work... { _id: 'Exit', _tag: 'Success', value: 'task1' } task2 interrupted */ ``` ### raceWith This function runs two effects concurrently and calls a specified "finisher" function once one of the effects completes, regardless of whether it succeeds or fails. The finisher functions for each effect allow you to handle the results of each effect as soon as they complete. The function takes two finisher callbacks, one for each effect, and allows you to specify how to handle the result of the race. This function is useful when you need to react to the completion of either effect without waiting for both to finish. It can be used whenever you want to take action based on the first available result. **Example** (Handling Results of Concurrent Tasks) ```ts twoslash import { Effect, Console } from "effect" const task1 = Effect.succeed("task1").pipe( Effect.delay("100 millis"), Effect.tap(Console.log("task1 done")), Effect.onInterrupt(() => Console.log("task1 interrupted").pipe(Effect.delay("100 millis")) ) ) const task2 = Effect.succeed("task2").pipe( Effect.delay("200 millis"), Effect.tap(Console.log("task2 done")), Effect.onInterrupt(() => Console.log("task2 interrupted").pipe(Effect.delay("100 millis")) ) ) const program = Effect.raceWith(task1, task2, { onSelfDone: (exit) => Console.log(`task1 exited with ${exit}`), onOtherDone: (exit) => Console.log(`task2 exited with ${exit}`) }) Effect.runFork(program) /* Output: task1 done task1 exited with { "_id": "Exit", "_tag": "Success", "value": "task1" } task2 interrupted */ ``` # [Deferred](https://effect.website/docs/concurrency/deferred/) ## Overview A `Deferred` is a specialized subtype of `Effect` that acts like a one-time variable with some unique characteristics. It can only be completed once, making it a useful tool for managing asynchronous operations and synchronization between different parts of your program. A deferred is essentially a synchronization primitive that represents a value that may not be available right away. When you create a deferred, it starts out empty. Later, it can be completed with either a success value `Success` or an error value `Error`: ```text showLineNumbers=false ┌─── Represents the success type │ ┌─── Represents the error type │ │ ▼ ▼ Deferred ``` Once completed, it cannot be changed again. When a fiber calls `Deferred.await`, it will pause until the deferred is completed. While the fiber is waiting, it doesn't block the thread, it only blocks semantically. This means other fibers can still run, ensuring efficient concurrency. A deferred is conceptually similar to JavaScript's `Promise`. The key difference is that it supports both success and error types, giving more type safety. ## Creating a Deferred A deferred can be created using the `Deferred.make` constructor. This returns an effect that represents the creation of the deferred. Since the creation of a deferred involves memory allocation, it must be done within an effect to ensure safe management of resources. **Example** (Creating a Deferred) ```ts twoslash import { Deferred } from "effect" // ┌─── Effect> // ▼ const deferred = Deferred.make() ``` ## Awaiting To retrieve a value from a deferred, you can use `Deferred.await`. This operation suspends the calling fiber until the deferred is completed with a value or an error. ```ts twoslash import { Effect, Deferred } from "effect" // ┌─── Effect, never, never> // ▼ const deferred = Deferred.make() // ┌─── Effect // ▼ const value = deferred.pipe(Effect.andThen(Deferred.await)) ``` ## Completing You can complete a deferred in several ways, depending on whether you want to succeed, fail, or interrupt the waiting fibers: | API | Description | | ----------------------- | --------------------------------------------------------------------------------------------------------------- | | `Deferred.succeed` | Completes the deferred successfully with a value. | | `Deferred.done` | Completes the deferred with an [Exit](/docs/data-types/exit/) value. | | `Deferred.complete` | Completes the deferred with the result of an effect. | | `Deferred.completeWith` | Completes the deferred with an effect. This effect will be executed by each waiting fiber, so use it carefully. | | `Deferred.fail` | Fails the deferred with an error. | | `Deferred.die` | Defects the deferred with a user-defined error. | | `Deferred.failCause` | Fails or defects the deferred with a [Cause](/docs/data-types/cause/). | | `Deferred.interrupt` | Interrupts the deferred, forcefully stopping or interrupting the waiting fibers. | **Example** (Completing a Deferred with Success) ```ts twoslash import { Effect, Deferred } from "effect" const program = Effect.gen(function* () { const deferred = yield* Deferred.make() // Complete the Deferred successfully yield* Deferred.succeed(deferred, 1) // Awaiting the Deferred to get its value const value = yield* Deferred.await(deferred) console.log(value) }) Effect.runFork(program) // Output: 1 ``` Completing a deferred produces an `Effect`. This effect returns `true` if the deferred was successfully completed, and `false` if it had already been completed previously. This can be useful for tracking the state of the deferred. **Example** (Checking Completion Status) ```ts twoslash import { Effect, Deferred } from "effect" const program = Effect.gen(function* () { const deferred = yield* Deferred.make() // Attempt to fail the Deferred const firstAttempt = yield* Deferred.fail(deferred, "oh no!") // Attempt to succeed after it has already been completed const secondAttempt = yield* Deferred.succeed(deferred, 1) console.log([firstAttempt, secondAttempt]) }) Effect.runFork(program) // Output: [ true, false ] ``` ## Checking Completion Status Sometimes, you might need to check if a deferred has been completed without suspending the fiber. This can be done using the `Deferred.poll` method. Here's how it works: - `Deferred.poll` returns an `Option>`: - If the `Deferred` is incomplete, it returns `None`. - If the `Deferred` is complete, it returns `Some`, which contains the result or error. Additionally, you can use the `Deferred.isDone` function to check if a deferred has been completed. This method returns an `Effect`, which evaluates to `true` if the `Deferred` is completed, allowing you to quickly check its state. **Example** (Polling and Checking Completion Status) ```ts twoslash import { Effect, Deferred } from "effect" const program = Effect.gen(function* () { const deferred = yield* Deferred.make() // Polling the Deferred to check if it's completed const done1 = yield* Deferred.poll(deferred) // Checking if the Deferred has been completed const done2 = yield* Deferred.isDone(deferred) console.log([done1, done2]) }) Effect.runFork(program) /* Output: [ { _id: 'Option', _tag: 'None' }, false ] */ ``` ## Common Use Cases `Deferred` becomes useful when you need to wait for something specific to happen in your program. It's ideal for scenarios where you want one part of your code to signal another part when it's ready. Here are a few common use cases: | **Use Case** | **Description** | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Coordinating Fibers** | When you have multiple concurrent tasks and need to coordinate their actions, `Deferred` can help one fiber signal to another when it has completed its task. | | **Synchronization** | Anytime you want to ensure that one piece of code doesn't proceed until another piece of code has finished its work, `Deferred` can provide the synchronization you need. | | **Handing Over Work** | You can use `Deferred` to hand over work from one fiber to another. For example, one fiber can prepare some data, and then a second fiber can continue processing it. | | **Suspending Execution** | When you want a fiber to pause its execution until some condition is met, a `Deferred` can be used to block it until the condition is satisfied. | **Example** (Using Deferred to Coordinate Two Fibers) In this example, a deferred is used to pass a value between two fibers. By running both fibers concurrently and using the deferred as a synchronization point, we can ensure that `fiberB` only proceeds after `fiberA` has completed its task. ```ts twoslash import { Effect, Deferred, Fiber } from "effect" const program = Effect.gen(function* () { const deferred = yield* Deferred.make() // Completes the Deferred with a value after a delay const taskA = Effect.gen(function* () { console.log("Starting task to complete the Deferred") yield* Effect.sleep("1 second") console.log("Completing the Deferred") return yield* Deferred.succeed(deferred, "hello world") }) // Waits for the Deferred and prints the value const taskB = Effect.gen(function* () { console.log("Starting task to get the value from the Deferred") const value = yield* Deferred.await(deferred) console.log("Got the value from the Deferred") return value }) // Run both fibers concurrently const fiberA = yield* Effect.fork(taskA) const fiberB = yield* Effect.fork(taskB) // Wait for both fibers to complete const both = yield* Fiber.join(Fiber.zip(fiberA, fiberB)) console.log(both) }) Effect.runFork(program) /* Starting task to complete the Deferred Starting task to get the value from the Deferred Completing the Deferred Got the value from the Deferred [ true, 'hello world' ] */ ``` # [Fibers](https://effect.website/docs/concurrency/fibers/) ## Overview import { Aside } from "@astrojs/starlight/components" Effect is a highly concurrent framework powered by fibers. Fibers are lightweight virtual threads with resource-safe cancellation capabilities, enabling many features in Effect. In this section, you will learn the basics of fibers and get familiar with some of the powerful low-level operators that utilize fibers. ## What Are Virtual Threads? JavaScript is inherently single-threaded, meaning it executes code in a single sequence of instructions. However, modern JavaScript environments use an event loop to manage asynchronous operations, creating the illusion of multitasking. In this context, virtual threads, or fibers, are logical threads simulated by the Effect runtime. They allow concurrent execution without relying on true multi-threading, which is not natively supported in JavaScript. ## How Fibers work All effects in Effect are executed by fibers. If you didn't create the fiber yourself, it was created by an operation you're using (if it's concurrent) or by the Effect runtime system. A fiber is created any time an effect is run. When running effects concurrently, a fiber is created for each concurrent effect. Even if you write "single-threaded" code with no concurrent operations, there will always be at least one fiber: the "main" fiber that executes your effect. Effect fibers have a well-defined lifecycle based on the effect they are executing. Every fiber exits with either a failure or success, depending on whether the effect it is executing fails or succeeds. Effect fibers have unique identities, local state, and a status (such as done, running, or suspended). To summarize: - An `Effect` is a higher-level concept that describes an effectful computation. It is lazy and immutable, meaning it represents a computation that may produce a value or fail but does not immediately execute. - A fiber, on the other hand, represents the running execution of an `Effect`. It can be interrupted or awaited to retrieve its result. Think of it as a way to control and interact with the ongoing computation. ## The Fiber Data Type The `Fiber` data type in Effect represents a "handle" on the execution of an effect. Here is the general form of a `Fiber`: ```text showLineNumbers=false ┌─── Represents the success type │ ┌─── Represents the error type │ │ ▼ ▼ Fiber ``` This type indicates that a fiber: - Succeeds and returns a value of type `Success` - Fails with an error of type `Error` Fibers do not have an `Requirements` type parameter because they only execute effects that have already had their requirements provided to them. ## Forking Effects You can create a new fiber by **forking** an effect. This starts the effect in a new fiber, and you receive a reference to that fiber. **Example** (Forking a Fiber) In this example, the Fibonacci calculation is forked into its own fiber, allowing it to run independently of the main fiber. The reference to the `fib10Fiber` can be used later to join or interrupt the fiber. ```ts twoslash import { Effect } from "effect" const fib = (n: number): Effect.Effect => n < 2 ? Effect.succeed(n) : Effect.zipWith(fib(n - 1), fib(n - 2), (a, b) => a + b) // ┌─── Effect, never, never> // ▼ const fib10Fiber = Effect.fork(fib(10)) ``` ## Joining Fibers One common operation with fibers is **joining** them. By using the `Fiber.join` function, you can wait for a fiber to complete and retrieve its result. The joined fiber will either succeed or fail, and the `Effect` returned by `join` reflects the outcome of the fiber. **Example** (Joining a Fiber) ```ts twoslash import { Effect, Fiber } from "effect" const fib = (n: number): Effect.Effect => n < 2 ? Effect.succeed(n) : Effect.zipWith(fib(n - 1), fib(n - 2), (a, b) => a + b) // ┌─── Effect, never, never> // ▼ const fib10Fiber = Effect.fork(fib(10)) const program = Effect.gen(function* () { // Retrieve the fiber const fiber = yield* fib10Fiber // Join the fiber and get the result const n = yield* Fiber.join(fiber) console.log(n) }) Effect.runFork(program) // Output: 55 ``` ## Awaiting Fibers The `Fiber.await` function is a helpful tool when working with fibers. It allows you to wait for a fiber to complete and retrieve detailed information about how it finished. The result is encapsulated in an [Exit](/docs/data-types/exit/) value, which gives you insight into whether the fiber succeeded, failed, or was interrupted. **Example** (Awaiting Fiber Completion) ```ts twoslash import { Effect, Fiber } from "effect" const fib = (n: number): Effect.Effect => n < 2 ? Effect.succeed(n) : Effect.zipWith(fib(n - 1), fib(n - 2), (a, b) => a + b) // ┌─── Effect, never, never> // ▼ const fib10Fiber = Effect.fork(fib(10)) const program = Effect.gen(function* () { // Retrieve the fiber const fiber = yield* fib10Fiber // Await its completion and get the Exit result const exit = yield* Fiber.await(fiber) console.log(exit) }) Effect.runFork(program) /* Output: { _id: 'Exit', _tag: 'Success', value: 55 } */ ``` ## Interruption Model While developing concurrent applications, there are several cases that we need to interrupt the execution of other fibers, for example: 1. A parent fiber might start some child fibers to perform a task, and later the parent might decide that, it doesn't need the result of some or all of the child fibers. 2. Two or more fibers start race with each other. The fiber whose result is computed first wins, and all other fibers are no longer needed, and should be interrupted. 3. In interactive applications, a user may want to stop some already running tasks, such as clicking on the "stop" button to prevent downloading more files. 4. Computations that run longer than expected should be aborted by using timeout operations. 5. When we have an application that perform compute-intensive tasks based on the user inputs, if the user changes the input we should cancel the current task and perform another one. ### Polling vs. Asynchronous Interruption When it comes to interrupting fibers, a naive approach is to allow one fiber to forcefully terminate another fiber. However, this approach is not ideal because it can leave shared state in an inconsistent and unreliable state if the target fiber is in the middle of modifying that state. Therefore, it does not guarantee internal consistency of the shared mutable state. Instead, there are two popular and valid solutions to tackle this problem: 1. **Semi-asynchronous Interruption (Polling for Interruption)**: Imperative languages often employ polling as a semi-asynchronous signaling mechanism, such as Java. In this model, a fiber sends an interruption request to another fiber. The target fiber continuously polls the interrupt status and checks whether it has received any interruption requests from other fibers. If an interruption request is detected, the target fiber terminates itself as soon as possible. With this solution, the fiber itself handles critical sections. So, if a fiber is in the middle of a critical section and receives an interruption request, it ignores the interruption and defers its handling until after the critical section. However, one drawback of this approach is that if the programmer forgets to poll regularly, the target fiber can become unresponsive, leading to deadlocks. Additionally, polling a global flag is not aligned with the functional paradigm followed by Effect. 2. **Asynchronous Interruption**: In asynchronous interruption, a fiber is allowed to terminate another fiber. The target fiber is not responsible for polling the interrupt status. Instead, during critical sections, the target fiber disables the interruptibility of those regions. This is a purely functional solution that doesn't require polling a global state. Effect adopts this solution for its interruption model, which is a fully asynchronous signaling mechanism. This mechanism overcomes the drawback of forgetting to poll regularly. It is also fully compatible with the functional paradigm because in a purely functional computation, we can abort the computation at any point, except during critical sections where interruption is disabled. ### Interrupting Fibers Fibers can be interrupted if their result is no longer needed. This action immediately stops the fiber and safely runs all finalizers to release any resources. Like `Fiber.await`, the `Fiber.interrupt` function returns an [Exit](/docs/data-types/exit/) value that provides detailed information about how the fiber ended. **Example** (Interrupting a Fiber) ```ts twoslash import { Effect, Fiber } from "effect" const program = Effect.gen(function* () { // Fork a fiber that runs indefinitely, printing "Hi!" const fiber = yield* Effect.fork( Effect.forever(Effect.log("Hi!").pipe(Effect.delay("10 millis"))) ) yield* Effect.sleep("30 millis") // Interrupt the fiber and get an Exit value detailing how it finished const exit = yield* Fiber.interrupt(fiber) console.log(exit) }) Effect.runFork(program) /* Output: timestamp=... level=INFO fiber=#1 message=Hi! timestamp=... level=INFO fiber=#1 message=Hi! { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Interrupt', fiberId: { _id: 'FiberId', _tag: 'Runtime', id: 0, startTimeMillis: ... } } } */ ``` By default, the effect returned by `Fiber.interrupt` waits until the fiber has fully terminated before resuming. This ensures that no new fibers are started before the previous ones have finished, a behavior known as "back-pressuring." If you do not require this waiting behavior, you can fork the interruption itself, allowing the main program to proceed without waiting for the fiber to terminate: **Example** (Forking an Interruption) ```ts twoslash import { Effect, Fiber } from "effect" const program = Effect.gen(function* () { const fiber = yield* Effect.fork( Effect.forever(Effect.log("Hi!").pipe(Effect.delay("10 millis"))) ) yield* Effect.sleep("30 millis") const _ = yield* Effect.fork(Fiber.interrupt(fiber)) console.log("Do something else...") }) Effect.runFork(program) /* Output: timestamp=... level=INFO fiber=#1 message=Hi! timestamp=... level=INFO fiber=#1 message=Hi! Do something else... */ ``` There is also a shorthand for background interruption called `Fiber.interruptFork`. ```ts twoslash del={8} ins={9} import { Effect, Fiber } from "effect" const program = Effect.gen(function* () { const fiber = yield* Effect.fork( Effect.forever(Effect.log("Hi!").pipe(Effect.delay("10 millis"))) ) yield* Effect.sleep("30 millis") // const _ = yield* Effect.fork(Fiber.interrupt(fiber)) const _ = yield* Fiber.interruptFork(fiber) console.log("Do something else...") }) Effect.runFork(program) /* Output: timestamp=... level=INFO fiber=#1 message=Hi! timestamp=... level=INFO fiber=#1 message=Hi! Do something else... */ ``` ## Composing Fibers The `Fiber.zip` and `Fiber.zipWith` functions allow you to combine two fibers into one. The resulting fiber will produce the results of both input fibers. If either fiber fails, the combined fiber will also fail. **Example** (Combining Fibers with `Fiber.zip`) In this example, both fibers run concurrently, and the results are combined into a tuple. ```ts twoslash import { Effect, Fiber } from "effect" const program = Effect.gen(function* () { // Fork two fibers that each produce a string const fiber1 = yield* Effect.fork(Effect.succeed("Hi!")) const fiber2 = yield* Effect.fork(Effect.succeed("Bye!")) // Combine the two fibers using Fiber.zip const fiber = Fiber.zip(fiber1, fiber2) // Join the combined fiber and get the result as a tuple const tuple = yield* Fiber.join(fiber) console.log(tuple) }) Effect.runFork(program) /* Output: [ 'Hi!', 'Bye!' ] */ ``` Another way to compose fibers is by using `Fiber.orElse`. This function allows you to provide an alternative fiber that will execute if the first one fails. If the first fiber succeeds, its result will be returned. If it fails, the second fiber will run instead, and its result will be returned regardless of its outcome. **Example** (Providing a Fallback Fiber with `Fiber.orElse`) ```ts twoslash import { Effect, Fiber } from "effect" const program = Effect.gen(function* () { // Fork a fiber that will fail const fiber1 = yield* Effect.fork(Effect.fail("Uh oh!")) // Fork another fiber that will succeed const fiber2 = yield* Effect.fork(Effect.succeed("Hurray!")) // If fiber1 fails, fiber2 will be used as a fallback const fiber = Fiber.orElse(fiber1, fiber2) const message = yield* Fiber.join(fiber) console.log(message) }) Effect.runFork(program) /* Output: Hurray! */ ``` ## Lifetime of Child Fibers When we fork fibers, depending on how we fork them we can have four different lifetime strategies for the child fibers: 1. **Fork With Automatic Supervision**. If we use the ordinary `Effect.fork` operation, the child fiber will be automatically supervised by the parent fiber. The lifetime child fibers are tied to the lifetime of their parent fiber. This means that these fibers will be terminated either when they end naturally, or when their parent fiber is terminated. 2. **Fork in Global Scope (Daemon)**. Sometimes we want to run long-running background fibers that aren't tied to their parent fiber, and also we want to fork them in a global scope. Any fiber that is forked in global scope will become daemon fiber. This can be achieved by using the `Effect.forkDaemon` operator. As these fibers have no parent, they are not supervised, and they will be terminated when they end naturally, or when our application is terminated. 3. **Fork in Local Scope**. Sometimes, we want to run a background fiber that isn't tied to its parent fiber, but we want to live that fiber in the local scope. We can fork fibers in the local scope by using `Effect.forkScoped`. Such fibers can outlive their parent fiber (so they are not supervised by their parents), and they will be terminated when their life end or their local scope is closed. 4. **Fork in Specific Scope**. This is similar to the previous strategy, but we can have more fine-grained control over the lifetime of the child fiber by forking it in a specific scope. We can do this by using the `Effect.forkIn` operator. ### Fork with Automatic Supervision Effect follows a **structured concurrency** model, where child fibers' lifetimes are tied to their parent. Simply put, the lifespan of a fiber depends on the lifespan of its parent fiber. **Example** (Automatically Supervised Child Fiber) In this scenario, the `parent` fiber spawns a `child` fiber that repeatedly prints a message every second. The `child` fiber will be terminated when the `parent` fiber completes. ```ts twoslash import { Effect, Console, Schedule } from "effect" // Child fiber that logs a message repeatedly every second const child = Effect.repeat( Console.log("child: still running!"), Schedule.fixed("1 second") ) const parent = Effect.gen(function* () { console.log("parent: started!") // Child fiber is supervised by the parent yield* Effect.fork(child) yield* Effect.sleep("3 seconds") console.log("parent: finished!") }) Effect.runFork(parent) /* Output: parent: started! child: still running! child: still running! child: still running! parent: finished! */ ``` This behavior can be extended to any level of nested fibers, ensuring a predictable and controlled fiber lifecycle. ### Fork in Global Scope (Daemon) You can create a long-running background fiber using `Effect.forkDaemon`. This type of fiber, known as a daemon fiber, is not tied to the lifecycle of its parent fiber. Instead, its lifetime is linked to the global scope. A daemon fiber continues running even if its parent fiber is terminated and will only stop when the global scope is closed or the fiber completes naturally. **Example** (Creating a Daemon Fiber) This example shows how daemon fibers can continue running in the background even after the parent fiber has finished. ```ts twoslash import { Effect, Console, Schedule } from "effect" // Daemon fiber that logs a message repeatedly every second const daemon = Effect.repeat( Console.log("daemon: still running!"), Schedule.fixed("1 second") ) const parent = Effect.gen(function* () { console.log("parent: started!") // Daemon fiber running independently yield* Effect.forkDaemon(daemon) yield* Effect.sleep("3 seconds") console.log("parent: finished!") }) Effect.runFork(parent) /* Output: parent: started! daemon: still running! daemon: still running! daemon: still running! parent: finished! daemon: still running! daemon: still running! daemon: still running! daemon: still running! daemon: still running! ...etc... */ ``` Even if the parent fiber is interrupted, the daemon fiber will continue running independently. **Example** (Interrupting the Parent Fiber) In this example, interrupting the parent fiber doesn't affect the daemon fiber, which continues to run in the background. ```ts twoslash import { Effect, Console, Schedule, Fiber } from "effect" // Daemon fiber that logs a message repeatedly every second const daemon = Effect.repeat( Console.log("daemon: still running!"), Schedule.fixed("1 second") ) const parent = Effect.gen(function* () { console.log("parent: started!") // Daemon fiber running independently yield* Effect.forkDaemon(daemon) yield* Effect.sleep("3 seconds") console.log("parent: finished!") }).pipe(Effect.onInterrupt(() => Console.log("parent: interrupted!"))) // Program that interrupts the parent fiber after 2 seconds const program = Effect.gen(function* () { const fiber = yield* Effect.fork(parent) yield* Effect.sleep("2 seconds") yield* Fiber.interrupt(fiber) // Interrupt the parent fiber }) Effect.runFork(program) /* Output: parent: started! daemon: still running! daemon: still running! parent: interrupted! daemon: still running! daemon: still running! daemon: still running! daemon: still running! daemon: still running! ...etc... */ ``` ### Fork in Local Scope Sometimes we want to create a fiber that is tied to a local [scope](/docs/resource-management/scope/), meaning its lifetime is not dependent on its parent fiber but is bound to the local scope in which it was forked. This can be done using the `Effect.forkScoped` operator. Fibers created with `Effect.forkScoped` can outlive their parent fibers and will only be terminated when the local scope itself is closed. **Example** (Forking a Fiber in a Local Scope) In this example, the `child` fiber continues to run beyond the lifetime of the `parent` fiber. The `child` fiber is tied to the local scope and will be terminated only when the scope ends. ```ts twoslash import { Effect, Console, Schedule } from "effect" // Child fiber that logs a message repeatedly every second const child = Effect.repeat( Console.log("child: still running!"), Schedule.fixed("1 second") ) // ┌─── Effect // ▼ const parent = Effect.gen(function* () { console.log("parent: started!") // Child fiber attached to local scope yield* Effect.forkScoped(child) yield* Effect.sleep("3 seconds") console.log("parent: finished!") }) // Program runs within a local scope const program = Effect.scoped( Effect.gen(function* () { console.log("Local scope started!") yield* Effect.fork(parent) // Scope lasts for 5 seconds yield* Effect.sleep("5 seconds") console.log("Leaving the local scope!") }) ) Effect.runFork(program) /* Output: Local scope started! parent: started! child: still running! child: still running! child: still running! parent: finished! child: still running! child: still running! Leaving the local scope! */ ``` ### Fork in Specific Scope There are some cases where we need more fine-grained control, so we want to fork a fiber in a specific scope. We can use the `Effect.forkIn` operator which takes the target scope as an argument. **Example** (Forking a Fiber in a Specific Scope) In this example, the `child` fiber is forked into the `outerScope`, allowing it to outlive the inner scope but still be terminated when the `outerScope` is closed. ```ts twoslash import { Console, Effect, Schedule } from "effect" // Child fiber that logs a message repeatedly every second const child = Effect.repeat( Console.log("child: still running!"), Schedule.fixed("1 second") ) const program = Effect.scoped( Effect.gen(function* () { yield* Effect.addFinalizer(() => Console.log("The outer scope is about to be closed!") ) // Capture the outer scope const outerScope = yield* Effect.scope // Create an inner scope yield* Effect.scoped( Effect.gen(function* () { yield* Effect.addFinalizer(() => Console.log("The inner scope is about to be closed!") ) // Fork the child fiber in the outer scope yield* Effect.forkIn(child, outerScope) yield* Effect.sleep("3 seconds") }) ) yield* Effect.sleep("5 seconds") }) ) Effect.runFork(program) /* Output: child: still running! child: still running! child: still running! The inner scope is about to be closed! child: still running! child: still running! child: still running! child: still running! child: still running! child: still running! The outer scope is about to be closed! */ ``` ## When do Fibers run? Forked fibers begin execution after the current fiber completes or yields. **Example** (Late Fiber Start Captures Only One Value) In the following example, the `changes` stream only captures a single value, `2`. This happens because the fiber created by `Effect.fork` starts **after** the value is updated. ```ts twoslash import { Effect, SubscriptionRef, Stream, Console } from "effect" const program = Effect.gen(function* () { const ref = yield* SubscriptionRef.make(0) yield* ref.changes.pipe( // Log each change in SubscriptionRef Stream.tap((n) => Console.log(`SubscriptionRef changed to ${n}`)), Stream.runDrain, // Fork a fiber to run the stream Effect.fork ) yield* SubscriptionRef.set(ref, 1) yield* SubscriptionRef.set(ref, 2) }) Effect.runFork(program) /* Output: SubscriptionRef changed to 2 */ ``` If you add a short delay with `Effect.sleep()` or call `Effect.yieldNow()`, you allow the current fiber to yield. This gives the forked fiber enough time to start and collect all values before they are updated. **Example** (Delay Allows Fiber to Capture All Values) ```ts twoslash ins={14} import { Effect, SubscriptionRef, Stream, Console } from "effect" const program = Effect.gen(function* () { const ref = yield* SubscriptionRef.make(0) yield* ref.changes.pipe( // Log each change in SubscriptionRef Stream.tap((n) => Console.log(`SubscriptionRef changed to ${n}`)), Stream.runDrain, // Fork a fiber to run the stream Effect.fork ) // Allow the fiber a chance to start yield* Effect.sleep("100 millis") yield* SubscriptionRef.set(ref, 1) yield* SubscriptionRef.set(ref, 2) }) Effect.runFork(program) /* Output: SubscriptionRef changed to 0 SubscriptionRef changed to 1 SubscriptionRef changed to 2 */ ``` # [Latch](https://effect.website/docs/concurrency/latch/) ## Overview A Latch is a synchronization tool that works like a gate, letting fibers wait until the latch is opened before they continue. The latch can be either open or closed: - When closed, fibers that reach the latch wait until it opens. - When open, fibers pass through immediately. Once opened, a latch typically stays open, although you can close it again if needed Imagine an application that processes requests only after completing an initial setup (like loading configuration data or establishing a database connection). You can create a latch in a closed state while the setup is happening. Any incoming requests, represented as fibers, would wait at the latch until it opens. Once the setup is finished, you call `latch.open` so the requests can proceed. ## The Latch Interface A `Latch` includes several operations that let you control and observe its state: | Operation | Description | | ---------- | -------------------------------------------------------------------------------------------------------- | | `whenOpen` | Runs a given effect only if the latch is open, otherwise, waits until it opens. | | `open` | Opens the latch so that any waiting fibers can proceed. | | `close` | Closes the latch, causing fibers to wait when they reach this latch in the future. | | `await` | Suspends the current fiber until the latch is opened. If the latch is already open, returns immediately. | | `release` | Allows waiting fibers to continue without permanently opening the latch. | ## Creating a Latch Use the `Effect.makeLatch` function to create a latch in an open or closed state by passing a boolean. The default is `false`, which means it starts closed. **Example** (Creating and Using a Latch) In this example, the latch starts closed. A fiber logs "open sesame" only when the latch is open. After waiting for one second, the latch is opened, releasing the fiber: ```ts twoslash import { Console, Effect } from "effect" // A generator function that demonstrates latch usage const program = Effect.gen(function* () { // Create a latch, starting in the closed state const latch = yield* Effect.makeLatch() // Fork a fiber that logs "open sesame" only when the latch is open const fiber = yield* Console.log("open sesame").pipe( latch.whenOpen, // Waits for the latch to open Effect.fork // Fork the effect into a new fiber ) // Wait for 1 second yield* Effect.sleep("1 second") // Open the latch, releasing the fiber yield* latch.open // Wait for the forked fiber to finish yield* fiber.await }) Effect.runFork(program) // Output: open sesame (after 1 second) ``` ## Latch vs Semaphore A latch is good when you have a one-time event or condition that determines whether fibers can proceed. For example, you might use a latch to block all fibers until a setup step is finished, and then open the latch so everyone can continue. A [semaphore](/docs/concurrency/semaphore/) with one lock (often called a binary semaphore or a mutex) is usually for mutual exclusion: it ensures that only one fiber at a time accesses a shared resource or section of code. Once a fiber acquires the lock, no other fiber can enter the protected area until the lock is released. In short: - Use a **latch** if you're gating a set of fibers on a specific event ("Wait here until this becomes true"). - Use a **semaphore (with one lock)** if you need to ensure only one fiber at a time is in a critical section or using a shared resource. # [PubSub](https://effect.website/docs/concurrency/pubsub/) ## Overview import { Aside } from "@astrojs/starlight/components" A `PubSub` serves as an asynchronous message hub, allowing publishers to send messages that can be received by all current subscribers. Unlike a [Queue](/docs/concurrency/queue/), where each value is delivered to only one consumer, a `PubSub` broadcasts each published message to all subscribers. This makes `PubSub` ideal for scenarios requiring message broadcasting rather than load distribution. ## Basic Operations A `PubSub` stores messages of type `A` and provides two fundamental operations: | API | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `PubSub.publish` | Sends a message of type `A` to the `PubSub`, returning an effect indicating if the message was successfully published. | | `PubSub.subscribe` | Creates a scoped effect that allows subscription to the `PubSub`, automatically unsubscribing when the scope ends. Subscribers receive messages through a [Dequeue](/docs/concurrency/queue/#dequeue) which holds published messages. | **Example** (Publishing a Message to Multiple Subscribers) ```ts twoslash import { Effect, PubSub, Queue } from "effect" const program = Effect.scoped( Effect.gen(function* () { const pubsub = yield* PubSub.bounded(2) // Two subscribers const dequeue1 = yield* PubSub.subscribe(pubsub) const dequeue2 = yield* PubSub.subscribe(pubsub) // Publish a message to the pubsub yield* PubSub.publish(pubsub, "Hello from a PubSub!") // Each subscriber receives the message console.log("Subscriber 1: " + (yield* Queue.take(dequeue1))) console.log("Subscriber 2: " + (yield* Queue.take(dequeue2))) }) ) Effect.runFork(program) /* Output: Subscriber 1: Hello from a PubSub! Subscriber 2: Hello from a PubSub! */ ``` ## Creating a PubSub ### Bounded PubSub A bounded `PubSub` applies back pressure to publishers when it reaches capacity, suspending additional publishing until space becomes available. Back pressure ensures that all subscribers receive all messages while they are subscribed. However, it can lead to slower message delivery if a subscriber is slow. **Example** (Bounded PubSub Creation) ```ts twoslash import { PubSub } from "effect" // Creates a bounded PubSub with a capacity of 2 const boundedPubSub = PubSub.bounded(2) ``` ### Dropping PubSub A dropping `PubSub` discards new values when full. The `PubSub.publish` operation returns `false` if the message is dropped. In a dropping pubsub, publishers can continue to publish new values, but subscribers are not guaranteed to receive all messages. **Example** (Dropping PubSub Creation) ```ts twoslash import { PubSub } from "effect" // Creates a dropping PubSub with a capacity of 2 const droppingPubSub = PubSub.dropping(2) ``` ### Sliding PubSub A sliding `PubSub` removes the oldest message to make space for new ones, ensuring that publishing never blocks. A sliding pubsub prevents slow subscribers from impacting the message delivery rate. However, there's still a risk that slow subscribers may miss some messages. **Example** (Sliding PubSub Creation) ```ts twoslash import { PubSub } from "effect" // Creates a sliding PubSub with a capacity of 2 const slidingPubSub = PubSub.sliding(2) ``` ### Unbounded PubSub An unbounded `PubSub` has no capacity limit, so publishing always succeeds immediately. Unbounded pubsubs guarantee that all subscribers receive all messages without slowing down message delivery. However, they can grow indefinitely if messages are published faster than they are consumed. Generally, it's recommended to use bounded, dropping, or sliding pubsubs unless you have specific use cases for unbounded pubsubs. **Example** ```ts twoslash import { PubSub } from "effect" // Creates an unbounded PubSub with unlimited capacity const unboundedPubSub = PubSub.unbounded() ``` ## Operators On PubSubs ### publishAll The `PubSub.publishAll` function lets you publish multiple values to the pubsub at once. **Example** (Publishing Multiple Messages) ```ts twoslash import { Effect, PubSub, Queue } from "effect" const program = Effect.scoped( Effect.gen(function* () { const pubsub = yield* PubSub.bounded(2) const dequeue = yield* PubSub.subscribe(pubsub) yield* PubSub.publishAll(pubsub, ["Message 1", "Message 2"]) console.log(yield* Queue.takeAll(dequeue)) }) ) Effect.runFork(program) /* Output: { _id: 'Chunk', values: [ 'Message 1', 'Message 2' ] } */ ``` ### capacity / size You can check the capacity and current size of a pubsub using `PubSub.capacity` and `PubSub.size`, respectively. Note that `PubSub.capacity` returns a `number` because the capacity is set at pubsub creation and never changes. In contrast, `PubSub.size` returns an effect that determines the current size of the pubsub since the number of messages in the pubsub can change over time. **Example** (Retrieving PubSub Capacity and Size) ```ts twoslash import { Effect, PubSub } from "effect" const program = Effect.gen(function* () { const pubsub = yield* PubSub.bounded(2) console.log(`capacity: ${PubSub.capacity(pubsub)}`) console.log(`size: ${yield* PubSub.size(pubsub)}`) }) Effect.runFork(program) /* Output: capacity: 2 size: 0 */ ``` ### Shutting Down a PubSub To shut down a pubsub, use `PubSub.shutdown`. You can also verify if it has been shut down with `PubSub.isShutdown`, or wait for the shutdown to complete with `PubSub.awaitShutdown`. Shutting down a pubsub also terminates all associated queues, ensuring that the shutdown signal is effectively communicated. ## PubSub as an Enqueue `PubSub` operators mirror those of [Queue](/docs/concurrency/queue/) with the main difference being that `PubSub.publish` and `PubSub.subscribe` are used in place of `Queue.offer` and `Queue.take`. If you're already familiar with using a `Queue`, you’ll find `PubSub` straightforward. Essentially, a `PubSub` can be seen as a `Enqueue` that only allows writes: ```ts twoslash showLineNumbers=false import type { Queue } from "effect" interface PubSub extends Queue.Enqueue {} ``` Here, the `Enqueue` type refers to a queue that only accepts enqueues (or writes). Any value enqueued here is published to the pubsub, and operations like shutdown will also affect the pubsub. This design makes `PubSub` highly flexible, letting you use it anywhere you need a `Enqueue` that only accepts published values. # [Queue](https://effect.website/docs/concurrency/queue/) ## Overview A `Queue` is a lightweight in-memory queue with built-in back-pressure, enabling asynchronous, purely-functional, and type-safe handling of data. ## Basic Operations A `Queue` stores values of type `A` and provides two fundamental operations: | API | Description | | ------------- | ---------------------------------------------------- | | `Queue.offer` | Adds a value of type `A` to the queue. | | `Queue.take` | Removes and returns the oldest value from the queue. | **Example** (Adding and Retrieving an Item) ```ts twoslash import { Effect, Queue } from "effect" const program = Effect.gen(function* () { // Creates a bounded queue with capacity 100 const queue = yield* Queue.bounded(100) // Adds 1 to the queue yield* Queue.offer(queue, 1) // Retrieves and removes the oldest value const value = yield* Queue.take(queue) return value }) Effect.runPromise(program).then(console.log) // Output: 1 ``` ## Creating a Queue Queues can be **bounded** (with a specified capacity) or **unbounded** (without a limit). Different types of queues handle new values differently when they reach capacity. ### Bounded Queue A bounded queue applies back-pressure when full, meaning any `Queue.offer` operation will suspend until there is space. **Example** (Creating a Bounded Queue) ```ts twoslash import { Queue } from "effect" // Creating a bounded queue with a capacity of 100 const boundedQueue = Queue.bounded(100) ``` ### Dropping Queue A dropping queue discards new values if the queue is full. **Example** (Creating a Dropping Queue) ```ts twoslash import { Queue } from "effect" // Creating a dropping queue with a capacity of 100 const droppingQueue = Queue.dropping(100) ``` ### Sliding Queue A sliding queue removes old values to make space for new ones when it reaches capacity. **Example** (Creating a Sliding Queue) ```ts twoslash import { Queue } from "effect" // Creating a sliding queue with a capacity of 100 const slidingQueue = Queue.sliding(100) ``` ### Unbounded Queue An unbounded queue has no capacity limit, allowing unrestricted additions. **Example** (Creating an Unbounded Queue) ```ts twoslash import { Queue } from "effect" // Creates an unbounded queue without a capacity limit const unboundedQueue = Queue.unbounded() ``` ## Adding Items to a Queue ### offer Use `Queue.offer` to add values to the queue. **Example** (Adding a Single Item) ```ts twoslash import { Effect, Queue } from "effect" const program = Effect.gen(function* () { const queue = yield* Queue.bounded(100) // Adds 1 to the queue yield* Queue.offer(queue, 1) }) ``` When using a back-pressured queue, `Queue.offer` suspends if the queue is full. To avoid blocking the main fiber, you can fork the `Queue.offer` operation. **Example** (Handling a Full Queue with `Effect.fork`) ```ts twoslash import { Effect, Queue, Fiber } from "effect" const program = Effect.gen(function* () { const queue = yield* Queue.bounded(1) // Fill the queue with one item yield* Queue.offer(queue, 1) // Attempting to add a second item will suspend as the queue is full const fiber = yield* Effect.fork(Queue.offer(queue, 2)) // Empties the queue to make space yield* Queue.take(queue) // Joins the fiber, completing the suspended offer yield* Fiber.join(fiber) // Returns the size of the queue after additions return yield* Queue.size(queue) }) Effect.runPromise(program).then(console.log) // Output: 1 ``` ### offerAll You can also add multiple items at once using `Queue.offerAll`. **Example** (Adding Multiple Items) ```ts twoslash import { Effect, Queue, Array } from "effect" const program = Effect.gen(function* () { const queue = yield* Queue.bounded(100) const items = Array.range(1, 10) // Adds all items to the queue at once yield* Queue.offerAll(queue, items) // Returns the size of the queue after additions return yield* Queue.size(queue) }) Effect.runPromise(program).then(console.log) // Output: 10 ``` ## Consuming Items from a Queue ### take The `Queue.take` operation removes and returns the oldest item from the queue. If the queue is empty, `Queue.take` will suspend and only resume when an item is added. To prevent blocking, you can fork the `Queue.take` operation into a new fiber. **Example** (Waiting for an Item in a Fiber) ```ts twoslash import { Effect, Queue, Fiber } from "effect" const program = Effect.gen(function* () { const queue = yield* Queue.bounded(100) // This take operation will suspend because the queue is empty const fiber = yield* Effect.fork(Queue.take(queue)) // Adds an item to the queue yield* Queue.offer(queue, "something") // Joins the fiber to get the result of the take operation const value = yield* Fiber.join(fiber) return value }) Effect.runPromise(program).then(console.log) // Output: something ``` ### poll To retrieve the queue's first item without suspending, use `Queue.poll`. If the queue is empty, `Queue.poll` returns `None`; if it has an item, it wraps it in `Some`. **Example** (Polling an Item) ```ts twoslash import { Effect, Queue } from "effect" const program = Effect.gen(function* () { const queue = yield* Queue.bounded(100) // Adds items to the queue yield* Queue.offer(queue, 10) yield* Queue.offer(queue, 20) // Retrieves the first item if available const head = yield* Queue.poll(queue) return head }) Effect.runPromise(program).then(console.log) /* Output: { _id: "Option", _tag: "Some", value: 10 } */ ``` ### takeUpTo To retrieve multiple items, use `Queue.takeUpTo`, which returns up to the specified number of items. If there aren't enough items, it returns all available items without waiting for more. This function is particularly useful for batch processing when an exact number of items is not required. It ensures the program continues working with whatever data is currently available. If you need to wait for an exact number of items before proceeding, consider using [takeN](#taken). **Example** (Taking Up to N Items) ```ts twoslash import { Effect, Queue } from "effect" const program = Effect.gen(function* () { const queue = yield* Queue.bounded(100) // Adds items to the queue yield* Queue.offer(queue, 1) yield* Queue.offer(queue, 2) yield* Queue.offer(queue, 3) // Retrieves up to 2 items const chunk = yield* Queue.takeUpTo(queue, 2) console.log(chunk) return "some result" }) Effect.runPromise(program).then(console.log) /* Output: { _id: 'Chunk', values: [ 1, 2 ] } some result */ ``` ### takeN Takes a specified number of elements from a queue. If the queue does not contain enough elements, the operation suspends until the required number of elements become available. This function is useful for scenarios where processing requires an exact number of items at a time, ensuring that the operation does not proceed until the batch is complete. **Example** (Taking a Fixed Number of Items) ```ts twoslash import { Effect, Queue, Fiber } from "effect" const program = Effect.gen(function* () { // Create a queue that can hold up to 100 elements const queue = yield* Queue.bounded(100) // Fork a fiber that attempts to take 3 items from the queue const fiber = yield* Effect.fork( Effect.gen(function* () { console.log("Attempting to take 3 items from the queue...") const chunk = yield* Queue.takeN(queue, 3) console.log(`Successfully took 3 items: ${chunk}`) }) ) // Offer only 2 items initially yield* Queue.offer(queue, 1) yield* Queue.offer(queue, 2) console.log( "Offered 2 items. The fiber is now waiting for the 3rd item..." ) // Simulate some delay yield* Effect.sleep("2 seconds") // Offer the 3rd item, which will unblock the takeN call yield* Queue.offer(queue, 3) console.log("Offered the 3rd item, which should unblock the fiber.") // Wait for the fiber to finish yield* Fiber.join(fiber) return "some result" }) Effect.runPromise(program).then(console.log) /* Output: Offered 2 items. The fiber is now waiting for the 3rd item... Attempting to take 3 items from the queue... Offered the 3rd item, which should unblock the fiber. Successfully took 3 items: { "_id": "Chunk", "values": [ 1, 2, 3 ] } some result */ ``` ### takeAll To retrieve all items from the queue at once, use `Queue.takeAll`. This operation completes immediately, returning an empty collection if the queue is empty. **Example** (Taking All Items) ```ts twoslash import { Effect, Queue } from "effect" const program = Effect.gen(function* () { const queue = yield* Queue.bounded(100) // Adds items to the queue yield* Queue.offer(queue, 10) yield* Queue.offer(queue, 20) yield* Queue.offer(queue, 30) // Retrieves all items from the queue const chunk = yield* Queue.takeAll(queue) return chunk }) Effect.runPromise(program).then(console.log) /* Output: { _id: "Chunk", values: [ 10, 20, 30 ] } */ ``` ## Shutting Down a Queue ### shutdown The `Queue.shutdown` operation allows you to interrupt all fibers that are currently suspended on `offer*` or `take*` operations. This action also empties the queue and makes any future `offer*` and `take*` calls terminate immediately. **Example** (Interrupting Fibers on Queue Shutdown) ```ts twoslash import { Effect, Queue, Fiber } from "effect" const program = Effect.gen(function* () { const queue = yield* Queue.bounded(3) // Forks a fiber that waits to take an item from the queue const fiber = yield* Effect.fork(Queue.take(queue)) // Shuts down the queue, interrupting the fiber yield* Queue.shutdown(queue) // Joins the interrupted fiber yield* Fiber.join(fiber) }) ``` ### awaitShutdown The `Queue.awaitShutdown` operation can be used to run an effect when the queue shuts down. It waits until the queue is closed and resumes immediately if the queue is already shut down. **Example** (Waiting for Queue Shutdown) ```ts twoslash import { Effect, Queue, Fiber, Console } from "effect" const program = Effect.gen(function* () { const queue = yield* Queue.bounded(3) // Forks a fiber to await queue shutdown and log a message const fiber = yield* Effect.fork( Queue.awaitShutdown(queue).pipe( Effect.andThen(Console.log("shutting down")) ) ) // Shuts down the queue, triggering the await in the fiber yield* Queue.shutdown(queue) yield* Fiber.join(fiber) }) Effect.runPromise(program) // Output: shutting down ``` ## Offer-only / Take-only Queues Sometimes, you might want certain parts of your code to only add values to a queue (`Enqueue`) or only retrieve values from a queue (`Dequeue`). Effect provides interfaces to enforce these specific capabilities. ### Enqueue All methods for adding values to a queue are defined by the `Enqueue` interface. This restricts the queue to only offer operations. **Example** (Restricting Queue to Offer-only Operations) ```ts twoslash import { Queue } from "effect" const send = (offerOnlyQueue: Queue.Enqueue, value: number) => { // This queue is restricted to offer operations only // Error: cannot use take on an offer-only queue // @ts-expect-error Queue.take(offerOnlyQueue) // Valid offer operation return Queue.offer(offerOnlyQueue, value) } ``` ### Dequeue Similarly, all methods for retrieving values from a queue are defined by the `Dequeue` interface, which restricts the queue to only take operations. **Example** (Restricting Queue to Take-only Operations) ```ts twoslash import { Queue } from "effect" const receive = (takeOnlyQueue: Queue.Dequeue) => { // This queue is restricted to take operations only // Error: cannot use offer on a take-only queue // @ts-expect-error Queue.offer(takeOnlyQueue, 1) // Valid take operation return Queue.take(takeOnlyQueue) } ``` The `Queue` type combines both `Enqueue` and `Dequeue`, so you can easily pass it to different parts of your code, enforcing only `Enqueue` or `Dequeue` behaviors as needed. **Example** (Using Offer-only and Take-only Queues Together) ```ts twoslash import { Effect, Queue } from "effect" const send = (offerOnlyQueue: Queue.Enqueue, value: number) => { return Queue.offer(offerOnlyQueue, value) } const receive = (takeOnlyQueue: Queue.Dequeue) => { return Queue.take(takeOnlyQueue) } const program = Effect.gen(function* () { const queue = yield* Queue.unbounded() // Add values to the queue yield* send(queue, 1) yield* send(queue, 2) // Retrieve values from the queue console.log(yield* receive(queue)) console.log(yield* receive(queue)) }) Effect.runFork(program) /* Output: 1 2 */ ``` # [Semaphore](https://effect.website/docs/concurrency/semaphore/) ## Overview import { Aside } from "@astrojs/starlight/components" A semaphore is a synchronization mechanism used to manage access to a shared resource. In Effect, semaphores help control resource access or coordinate tasks within asynchronous, concurrent operations. A semaphore acts as a generalized mutex, allowing a set number of **permits** to be held and released concurrently. Permits act like tickets, giving tasks or fibers controlled access to a shared resource. When no permits are available, tasks trying to acquire one will wait until a permit is released. ## Creating a Semaphore The `Effect.makeSemaphore` function initializes a semaphore with a specified number of permits. Each permit allows one task to access a resource or perform an operation concurrently, and multiple permits enable a configurable level of concurrency. **Example** (Creating a Semaphore with 3 Permits) ```ts twoslash import { Effect } from "effect" // Create a semaphore with 3 permits const mutex = Effect.makeSemaphore(3) ``` ## withPermits The `withPermits` method lets you specify the number of permits required to run an effect. Once the specified permits are available, it runs the effect, automatically releasing the permits when the task completes. **Example** (Forcing Sequential Task Execution with a One-Permit Semaphore) In this example, three tasks are started concurrently, but they run sequentially because the one-permit semaphore only allows one task to proceed at a time. ```ts twoslash import { Effect } from "effect" const task = Effect.gen(function* () { yield* Effect.log("start") yield* Effect.sleep("2 seconds") yield* Effect.log("end") }) const program = Effect.gen(function* () { const mutex = yield* Effect.makeSemaphore(1) // Wrap the task to require one permit, forcing sequential execution const semTask = mutex .withPermits(1)(task) .pipe(Effect.withLogSpan("elapsed")) // Run 3 tasks concurrently, but they execute sequentially // due to the one-permit semaphore yield* Effect.all([semTask, semTask, semTask], { concurrency: "unbounded" }) }) Effect.runFork(program) /* Output: timestamp=... level=INFO fiber=#1 message=start elapsed=3ms timestamp=... level=INFO fiber=#1 message=end elapsed=2010ms timestamp=... level=INFO fiber=#2 message=start elapsed=2012ms timestamp=... level=INFO fiber=#2 message=end elapsed=4017ms timestamp=... level=INFO fiber=#3 message=start elapsed=4018ms timestamp=... level=INFO fiber=#3 message=end elapsed=6026ms */ ``` **Example** (Using Multiple Permits to Control Concurrent Task Execution) In this example, we create a semaphore with five permits and use `withPermits(n)` to allocate a different number of permits for each task: ```ts twoslash import { Effect } from "effect" const program = Effect.gen(function* () { const mutex = yield* Effect.makeSemaphore(5) const tasks = [1, 2, 3, 4, 5].map((n) => mutex .withPermits(n)( Effect.delay(Effect.log(`process: ${n}`), "2 seconds") ) .pipe(Effect.withLogSpan("elapsed")) ) yield* Effect.all(tasks, { concurrency: "unbounded" }) }) Effect.runFork(program) /* Output: timestamp=... level=INFO fiber=#1 message="process: 1" elapsed=2011ms timestamp=... level=INFO fiber=#2 message="process: 2" elapsed=2017ms timestamp=... level=INFO fiber=#3 message="process: 3" elapsed=4020ms timestamp=... level=INFO fiber=#4 message="process: 4" elapsed=6025ms timestamp=... level=INFO fiber=#5 message="process: 5" elapsed=8034ms */ ``` # [BigDecimal](https://effect.website/docs/data-types/bigdecimal/) ## Overview import { Aside } from "@astrojs/starlight/components" In JavaScript, numbers are typically stored as 64-bit floating-point values. While floating-point numbers are fast and versatile, they can introduce small rounding errors. These are often hard to notice in everyday usage but can become problematic in areas like finance or statistics, where small inaccuracies may lead to larger discrepancies over time. By using the BigDecimal module, you can avoid these issues and perform calculations with a higher degree of precision. The `BigDecimal` data type can represent real numbers with a large number of decimal places, preventing the common errors of floating-point math (for example, 0.1 + 0.2 ≠ 0.3). ## How BigDecimal Works A `BigDecimal` represents a number using two components: 1. `value`: A `BigInt` that stores the digits of the number. 2. `scale`: A 64-bit integer that determines the position of the decimal point. The number represented by a `BigDecimal` is calculated as: value x 10-scale. - If `scale` is zero or positive, it specifies the number of digits to the right of the decimal point. - If `scale` is negative, the `value` is multiplied by 10 raised to the power of the negated scale. For example: - A `BigDecimal` with `value = 12345n` and `scale = 2` represents `123.45`. - A `BigDecimal` with `value = 12345n` and `scale = -2` represents `1234500`. The maximum precision is large but not infinite, limited to 263 decimal places. ## Creating a BigDecimal ### make The `make` function creates a `BigDecimal` by specifying a `BigInt` value and a scale. The `scale` determines the number of digits to the right of the decimal point. **Example** (Creating a BigDecimal with a Specified Scale) ```ts twoslash import { BigDecimal } from "effect" // Create a BigDecimal from a BigInt (1n) with a scale of 2 const decimal = BigDecimal.make(1n, 2) console.log(decimal) // Output: { _id: 'BigDecimal', value: '1', scale: 2 } // Convert the BigDecimal to a string console.log(String(decimal)) // Output: BigDecimal(0.01) // Format the BigDecimal as a standard decimal string console.log(BigDecimal.format(decimal)) // Output: 0.01 // Convert the BigDecimal to exponential notation console.log(BigDecimal.toExponential(decimal)) // Output: 1e-2 ``` ### fromBigInt The `fromBigInt` function creates a `BigDecimal` from a `bigint`. The `scale` defaults to `0`, meaning the number has no fractional part. **Example** (Creating a BigDecimal from a BigInt) ```ts twoslash import { BigDecimal } from "effect" const decimal = BigDecimal.fromBigInt(10n) console.log(decimal) // Output: { _id: 'BigDecimal', value: '10', scale: 0 } ``` ### fromString Parses a numerical string into a `BigDecimal`. Returns an `Option`: - `Some(BigDecimal)` if the string is valid. - `None` if the string is invalid. **Example** (Parsing a String into a BigDecimal) ```ts twoslash import { BigDecimal } from "effect" const decimal = BigDecimal.fromString("0.02") console.log(decimal) /* Output: { _id: 'Option', _tag: 'Some', value: { _id: 'BigDecimal', value: '2', scale: 2 } } */ ``` ### unsafeFromString The `unsafeFromString` function is a variant of `fromString` that throws an error if the input string is invalid. Use this only when you are confident that the input will always be valid. **Example** (Unsafe Parsing of a String) ```ts twoslash import { BigDecimal } from "effect" const decimal = BigDecimal.unsafeFromString("0.02") console.log(decimal) // Output: { _id: 'BigDecimal', value: '2', scale: 2 } ``` ### unsafeFromNumber Creates a `BigDecimal` from a JavaScript `number`. Throws a `RangeError` for non-finite numbers (`NaN`, `+Infinity`, or `-Infinity`). **Example** (Unsafe Parsing of a Number) ```ts twoslash import { BigDecimal } from "effect" console.log(BigDecimal.unsafeFromNumber(123.456)) // Output: { _id: 'BigDecimal', value: '123456', scale: 3 } ``` ## Basic Arithmetic Operations The BigDecimal module supports a variety of arithmetic operations that provide precision and avoid the rounding errors common in standard JavaScript arithmetic. Below is a list of supported operations: | Function | Description | | ----------------- | -------------------------------------------------------------------------------------------------------------- | | `sum` | Adds two `BigDecimal` values. | | `subtract` | Subtracts one `BigDecimal` value from another. | | `multiply` | Multiplies two `BigDecimal` values. | | `divide` | Divides one `BigDecimal` value by another, returning an `Option`. | | `unsafeDivide` | Divides one `BigDecimal` value by another, throwing an error if the divisor is zero. | | `negate` | Negates a `BigDecimal` value (i.e., changes its sign). | | `remainder` | Returns the remainder of dividing one `BigDecimal` value by another, returning an `Option`. | | `unsafeRemainder` | Returns the remainder of dividing one `BigDecimal` value by another, throwing an error if the divisor is zero. | | `sign` | Returns the sign of a `BigDecimal` value (`-1`, `0`, or `1`). | | `abs` | Returns the absolute value of a `BigDecimal`. | **Example** (Performing Basic Arithmetic with BigDecimal) ```ts twoslash import { BigDecimal } from "effect" const dec1 = BigDecimal.unsafeFromString("1.05") const dec2 = BigDecimal.unsafeFromString("2.10") // Addition console.log(String(BigDecimal.sum(dec1, dec2))) // Output: BigDecimal(3.15) // Multiplication console.log(String(BigDecimal.multiply(dec1, dec2))) // Output: BigDecimal(2.205) // Subtraction console.log(String(BigDecimal.subtract(dec2, dec1))) // Output: BigDecimal(1.05) // Division (safe, returns Option) console.log(BigDecimal.divide(dec2, dec1)) /* Output: { _id: 'Option', _tag: 'Some', value: { _id: 'BigDecimal', value: '2', scale: 0 } } */ // Division (unsafe, throws if divisor is zero) console.log(String(BigDecimal.unsafeDivide(dec2, dec1))) // Output: BigDecimal(2) // Negation console.log(String(BigDecimal.negate(dec1))) // Output: BigDecimal(-1.05) // Modulus (unsafe, throws if divisor is zero) console.log( String( BigDecimal.unsafeRemainder(dec2, BigDecimal.unsafeFromString("0.6")) ) ) // Output: BigDecimal(0.3) ``` Using `BigDecimal` for arithmetic operations helps to avoid the inaccuracies commonly encountered with floating-point numbers in JavaScript. For example: **Example** (Avoiding Floating-Point Errors) ```ts twoslash const dec1 = 1.05 const dec2 = 2.1 console.log(String(dec1 + dec2)) // Output: 3.1500000000000004 ``` ## Comparison Operations The `BigDecimal` module provides several functions for comparing decimal values. These allow you to determine the relative order of two values, find the minimum or maximum, and check specific properties like positivity or integer status. ### Comparison Functions | Function | Description | | ---------------------- | ------------------------------------------------------------------------ | | `lessThan` | Checks if the first `BigDecimal` is smaller than the second. | | `lessThanOrEqualTo` | Checks if the first `BigDecimal` is smaller than or equal to the second. | | `greaterThan` | Checks if the first `BigDecimal` is larger than the second. | | `greaterThanOrEqualTo` | Checks if the first `BigDecimal` is larger than or equal to the second. | | `min` | Returns the smaller of two `BigDecimal` values. | | `max` | Returns the larger of two `BigDecimal` values. | **Example** (Comparing Two BigDecimal Values) ```ts twoslash import { BigDecimal } from "effect" const dec1 = BigDecimal.unsafeFromString("1.05") const dec2 = BigDecimal.unsafeFromString("2.10") console.log(BigDecimal.lessThan(dec1, dec2)) // Output: true console.log(BigDecimal.lessThanOrEqualTo(dec1, dec2)) // Output: true console.log(BigDecimal.greaterThan(dec1, dec2)) // Output: false console.log(BigDecimal.greaterThanOrEqualTo(dec1, dec2)) // Output: false console.log(BigDecimal.min(dec1, dec2)) // Output: { _id: 'BigDecimal', value: '105', scale: 2 } console.log(BigDecimal.max(dec1, dec2)) // Output: { _id: 'BigDecimal', value: '210', scale: 2 } ``` ### Predicates for Comparison The module also includes predicates to check specific properties of a `BigDecimal`: | Predicate | Description | | ------------ | -------------------------------------------------------------- | | `isZero` | Checks if the value is exactly zero. | | `isPositive` | Checks if the value is positive. | | `isNegative` | Checks if the value is negative. | | `between` | Checks if the value lies within a specified range (inclusive). | | `isInteger` | Checks if the value is an integer (i.e., no fractional part). | **Example** (Checking the Sign and Properties of BigDecimal Values) ```ts twoslash import { BigDecimal } from "effect" const dec1 = BigDecimal.unsafeFromString("1.05") const dec2 = BigDecimal.unsafeFromString("-2.10") console.log(BigDecimal.isZero(BigDecimal.unsafeFromString("0"))) // Output: true console.log(BigDecimal.isPositive(dec1)) // Output: true console.log(BigDecimal.isNegative(dec2)) // Output: true console.log( BigDecimal.between({ minimum: BigDecimal.unsafeFromString("1"), maximum: BigDecimal.unsafeFromString("2") })(dec1) ) // Output: true console.log( BigDecimal.isInteger(dec2), BigDecimal.isInteger(BigDecimal.fromBigInt(3n)) ) // Output: false true ``` ## Normalization and Equality In some cases, two `BigDecimal` values can have different internal representations but still represent the same number. For example, `1.05` could be internally represented with different scales, such as: - `105n` with a scale of `2` - `1050n` with a scale of `3` To ensure consistency, you can normalize a `BigDecimal` to adjust the scale and remove trailing zeros. ### Normalization The `BigDecimal.normalize` function adjusts the scale of a `BigDecimal` and eliminates any unnecessary trailing zeros in its internal representation. **Example** (Normalizing a BigDecimal) ```ts twoslash import { BigDecimal } from "effect" const dec = BigDecimal.make(1050n, 3) console.log(BigDecimal.normalize(dec)) // Output: { _id: 'BigDecimal', value: '105', scale: 2 } ``` ### Equality To check if two `BigDecimal` values are numerically equal, regardless of their internal representation, use the `BigDecimal.equals` function. **Example** (Checking Equality) ```ts twoslash import { BigDecimal } from "effect" const dec1 = BigDecimal.make(105n, 2) const dec2 = BigDecimal.make(1050n, 3) console.log(BigDecimal.equals(dec1, dec2)) // Output: true ``` # [Cause](https://effect.website/docs/data-types/cause/) ## Overview The [`Effect`](/docs/getting-started/the-effect-type/) type is polymorphic in error type `E`, allowing flexibility in handling any desired error type. However, there is often additional information about failures that the error type `E` alone does not capture. To address this, Effect uses the `Cause` data type to store various details such as: - Unexpected errors or defects - Stack and execution traces - Reasons for fiber interruptions Effect strictly preserves all failure-related information, storing a full picture of the error context in the `Cause` type. This comprehensive approach enables precise analysis and handling of failures, ensuring no data is lost. Though `Cause` values aren't typically manipulated directly, they underlie errors within Effect workflows, providing access to both concurrent and sequential error details. This allows for thorough error analysis when needed. ## Creating Causes You can intentionally create an effect with a specific cause using `Effect.failCause`. **Example** (Defining Effects with Different Causes) ```ts twoslash import { Effect, Cause } from "effect" // Define an effect that dies with an unexpected error // // ┌─── Effect // ▼ const die = Effect.failCause(Cause.die("Boom!")) // Define an effect that fails with an expected error // // ┌─── Effect // ▼ const fail = Effect.failCause(Cause.fail("Oh no!")) ``` Some causes do not influence the error type of the effect, leading to `never` in the error channel: ```text showLineNumbers=false ┌─── no error information ▼ Effect ``` For instance, `Cause.die` does not specify an error type for the effect, while `Cause.fail` does, setting the error channel type accordingly. ## Cause Variations There are several causes for various errors, in this section, we will describe each of these causes. ### Empty The `Empty` cause signifies the absence of any errors. ### Fail The `Fail` cause represents a failure due to an expected error of type `E`. ### Die The `Die` cause indicates a failure resulting from a defect, which is an unexpected or unintended error. ### Interrupt The `Interrupt` cause represents a failure due to `Fiber` interruption and contains the `FiberId` of the interrupted `Fiber`. ### Sequential The `Sequential` cause combines two causes that occurred one after the other. For example, in an `Effect.ensuring` operation (analogous to `try-finally`), if both the `try` and `finally` sections fail, the two errors are represented in sequence by a `Sequential` cause. **Example** (Capturing Sequential Failures with a `Sequential` Cause) ```ts twoslash import { Effect, Cause } from "effect" const program = Effect.failCause(Cause.fail("Oh no!")).pipe( Effect.ensuring(Effect.failCause(Cause.die("Boom!"))) ) Effect.runPromiseExit(program).then(console.log) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Sequential', left: { _id: 'Cause', _tag: 'Fail', failure: 'Oh no!' }, right: { _id: 'Cause', _tag: 'Die', defect: 'Boom!' } } } */ ``` ### Parallel The `Parallel` cause combines two causes that occurred concurrently. In Effect programs, two operations may run in parallel, potentially leading to multiple failures. When both computations fail simultaneously, a `Parallel` cause represents the concurrent errors within the effect workflow. **Example** (Capturing Concurrent Failures with a `Parallel` Cause) ```ts twoslash import { Effect, Cause } from "effect" const program = Effect.all( [ Effect.failCause(Cause.fail("Oh no!")), Effect.failCause(Cause.die("Boom!")) ], { concurrency: 2 } ) Effect.runPromiseExit(program).then(console.log) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Parallel', left: { _id: 'Cause', _tag: 'Fail', failure: 'Oh no!' }, right: { _id: 'Cause', _tag: 'Die', defect: 'Boom!' } } } */ ``` ## Retrieving the Cause of an Effect To retrieve the cause of a failed effect, use `Effect.cause`. This allows you to inspect or handle the exact reason behind the failure. **Example** (Retrieving and Inspecting a Failure Cause) ```ts twoslash import { Effect } from "effect" const program = Effect.gen(function* () { const cause = yield* Effect.cause(Effect.fail("Oh no!")) console.log(cause) }) Effect.runPromise(program) /* Output: { _id: 'Cause', _tag: 'Fail', failure: 'Oh no!' } */ ``` ## Guards To determine the specific type of a `Cause`, use the guards provided in the Cause module: - `Cause.isEmpty`: Checks if the cause is empty, indicating no error. - `Cause.isFailType`: Identifies causes that represent an expected failure. - `Cause.isDie`: Identifies causes that represent an unexpected defect. - `Cause.isInterruptType`: Identifies causes related to fiber interruptions. - `Cause.isSequentialType`: Checks if the cause consists of sequential errors. - `Cause.isParallelType`: Checks if the cause contains parallel errors. **Example** (Using Guards to Identify Cause Types) ```ts twoslash import { Cause } from "effect" const cause = Cause.fail(new Error("my message")) if (Cause.isFailType(cause)) { console.log(cause.error.message) // Output: my message } ``` These guards allow you to accurately identify the type of a `Cause`, making it easier to handle various error cases in your code. Whether dealing with expected failures, unexpected defects, interruptions, or composite errors, these guards provide a clear method for assessing and managing error scenarios. ## Pattern Matching The `Cause.match` function provides a straightforward way to handle each case of a `Cause`. By defining callbacks for each possible cause type, you can respond to specific error scenarios with custom behavior. **Example** (Pattern Matching on Different Causes) ```ts twoslash import { Cause } from "effect" const cause = Cause.parallel( Cause.fail(new Error("my fail message")), Cause.die("my die message") ) console.log( Cause.match(cause, { onEmpty: "(empty)", onFail: (error) => `(error: ${error.message})`, onDie: (defect) => `(defect: ${defect})`, onInterrupt: (fiberId) => `(fiberId: ${fiberId})`, onSequential: (left, right) => `(onSequential (left: ${left}) (right: ${right}))`, onParallel: (left, right) => `(onParallel (left: ${left}) (right: ${right})` }) ) /* Output: (onParallel (left: (error: my fail message)) (right: (defect: my die message)) */ ``` ## Pretty Printing Clear and readable error messages are key for effective debugging. The `Cause.pretty` function helps by formatting error messages in a structured way, making it easier to understand failure details. **Example** (Using `Cause.pretty` for Readable Error Messages) ```ts twoslash import { Cause, FiberId } from "effect" console.log(Cause.pretty(Cause.empty)) /* Output: All fibers interrupted without errors. */ console.log(Cause.pretty(Cause.fail(new Error("my fail message")))) /* Output: Error: my fail message ...stack trace... */ console.log(Cause.pretty(Cause.die("my die message"))) /* Output: Error: my die message */ console.log(Cause.pretty(Cause.interrupt(FiberId.make(1, 0)))) /* Output: All fibers interrupted without errors. */ console.log( Cause.pretty(Cause.sequential(Cause.fail("fail1"), Cause.fail("fail2"))) ) /* Output: Error: fail1 Error: fail2 */ ``` ## Retrieval of Failures and Defects To specifically collect failures or defects from a `Cause`, you can use `Cause.failures` and `Cause.defects`. These functions allow you to inspect only the errors or unexpected defects that occurred. **Example** (Extracting Failures and Defects from a Cause) ```ts twoslash import { Effect, Cause } from "effect" const program = Effect.gen(function* () { const cause = yield* Effect.cause( Effect.all([ Effect.fail("error 1"), Effect.die("defect"), Effect.fail("error 2") ]) ) console.log(Cause.failures(cause)) console.log(Cause.defects(cause)) }) Effect.runPromise(program) /* Output: { _id: 'Chunk', values: [ 'error 1' ] } { _id: 'Chunk', values: [] } */ ``` # [Chunk](https://effect.website/docs/data-types/chunk/) ## Overview import { Aside } from "@astrojs/starlight/components" A `Chunk` represents an ordered, immutable collection of values of type `A`. While similar to an array, `Chunk` provides a functional interface, optimizing certain operations that can be costly with regular arrays, like repeated concatenation. ## Why Use Chunk? - **Immutability**: Unlike standard JavaScript arrays, which are mutable, `Chunk` provides a truly immutable collection, preventing data from being modified after creation. This is especially useful in concurrent programming contexts where immutability can enhance data consistency. - **High Performance**: `Chunk` supports specialized operations for efficient array manipulation, such as appending single elements or concatenating chunks, making these operations faster than their regular JavaScript array equivalents. ## Creating a Chunk ### empty Create an empty `Chunk` with `Chunk.empty`. **Example** (Creating an Empty Chunk) ```ts twoslash import { Chunk } from "effect" // ┌─── Chunk // ▼ const chunk = Chunk.empty() ``` ### make To create a `Chunk` with specific values, use `Chunk.make(...values)`. Note that the resulting chunk is typed as non-empty. **Example** (Creating a Non-Empty Chunk) ```ts twoslash import { Chunk } from "effect" // ┌─── NonEmptyChunk // ▼ const chunk = Chunk.make(1, 2, 3) ``` ### fromIterable You can create a `Chunk` by providing a collection, either from an iterable or directly from an array. **Example** (Creating a Chunk from an Iterable) ```ts twoslash import { Chunk, List } from "effect" const fromArray = Chunk.fromIterable([1, 2, 3]) const fromList = Chunk.fromIterable(List.make(1, 2, 3)) ``` ### unsafeFromArray `Chunk.unsafeFromArray` creates a `Chunk` directly from an array without cloning. This approach can improve performance by avoiding the overhead of copying data but requires caution, as it bypasses the usual immutability guarantees. **Example** (Directly Creating a Chunk from an Array) ```ts twoslash import { Chunk } from "effect" const chunk = Chunk.unsafeFromArray([1, 2, 3]) ``` ## Concatenating To combine two `Chunk` instances into one, use `Chunk.appendAll`. **Example** (Combining Two Chunks into One) ```ts twoslash import { Chunk } from "effect" // Concatenate two chunks with different types of elements // // ┌─── NonEmptyChunk // ▼ const chunk = Chunk.appendAll(Chunk.make(1, 2), Chunk.make("a", "b")) console.log(chunk) /* Output: { _id: 'Chunk', values: [ 1, 2, 'a', 'b' ] } */ ``` ## Dropping To remove elements from the beginning of a `Chunk`, use `Chunk.drop`, specifying the number of elements to discard. **Example** (Dropping Elements from the Start) ```ts twoslash import { Chunk } from "effect" // Drops the first 2 elements from the Chunk const chunk = Chunk.drop(Chunk.make(1, 2, 3, 4), 2) ``` ## Comparing To check if two `Chunk` instances are equal, use [`Equal.equals`](/docs/trait/equal/). This function compares the contents of each `Chunk` for structural equality. **Example** (Comparing Two Chunks) ```ts twoslash import { Chunk, Equal } from "effect" const chunk1 = Chunk.make(1, 2) const chunk2 = Chunk.make(1, 2, 3) console.log(Equal.equals(chunk1, chunk1)) // Output: true console.log(Equal.equals(chunk1, chunk2)) // Output: false console.log(Equal.equals(chunk1, Chunk.make(1, 2))) // Output: true ``` ## Converting Convert a `Chunk` to a `ReadonlyArray` using `Chunk.toReadonlyArray`. The resulting type varies based on the `Chunk`'s contents, distinguishing between empty, non-empty, and generic chunks. **Example** (Converting a Chunk to a ReadonlyArray) ```ts twoslash import { Chunk } from "effect" // ┌─── readonly [number, ...number[]] // ▼ const nonEmptyArray = Chunk.toReadonlyArray(Chunk.make(1, 2, 3)) // ┌─── readonly never[] // ▼ const emptyArray = Chunk.toReadonlyArray(Chunk.empty()) declare const chunk: Chunk.Chunk // ┌─── readonly number[] // ▼ const array = Chunk.toReadonlyArray(chunk) ``` # [Data](https://effect.website/docs/data-types/data/) ## Overview import { Aside } from "@astrojs/starlight/components" The Data module simplifies creating and handling data structures in TypeScript. It provides tools for **defining data types**, ensuring **equality** between objects, and **hashing** data for efficient comparisons. ## Value Equality The Data module provides constructors for creating data types with built-in support for equality and hashing, eliminating the need for custom implementations. This means that two values created using these constructors are considered equal if they have the same structure and values. ### struct In plain JavaScript, objects are considered equal only if they refer to the exact same instance. **Example** (Comparing Two Objects in Plain JavaScript) ```ts twoslash const alice = { name: "Alice", age: 30 } // This comparison is false because they are different instances // @ts-expect-error console.log(alice === { name: "Alice", age: 30 }) // Output: false ``` However, the `Data.struct` constructor allows you to compare values based on their structure and content. **Example** (Creating and Checking Equality of Structs) ```ts twoslash import { Data, Equal } from "effect" // ┌─── { readonly name: string; readonly age: number; } // ▼ const alice = Data.struct({ name: "Alice", age: 30 }) // Check if Alice is equal to a new object // with the same structure and values console.log(Equal.equals(alice, Data.struct({ name: "Alice", age: 30 }))) // Output: true // Check if Alice is equal to a plain JavaScript object // with the same content console.log(Equal.equals(alice, { name: "Alice", age: 30 })) // Output: false ``` The comparison performed by `Equal.equals` is **shallow**, meaning nested objects are not compared recursively unless they are also created using `Data.struct`. **Example** (Shallow Comparison with Nested Objects) ```ts twoslash import { Data, Equal } from "effect" const nested = Data.struct({ name: "Alice", nested_field: { value: 42 } }) // This will be false because the nested objects are compared by reference console.log( Equal.equals( nested, Data.struct({ name: "Alice", nested_field: { value: 42 } }) ) ) // Output: false ``` To ensure nested objects are compared by structure, use `Data.struct` for them as well. **Example** (Correctly Comparing Nested Objects) ```ts twoslash import { Data, Equal } from "effect" const nested = Data.struct({ name: "Alice", nested_field: Data.struct({ value: 42 }) }) // Now, the comparison returns true console.log( Equal.equals( nested, Data.struct({ name: "Alice", nested_field: Data.struct({ value: 42 }) }) ) ) // Output: true ``` ### tuple To represent your data using tuples, you can use the `Data.tuple` constructor. This ensures that your tuples can be compared structurally. **Example** (Creating and Checking Equality of Tuples) ```ts twoslash import { Data, Equal } from "effect" // ┌─── readonly [string, number] // ▼ const alice = Data.tuple("Alice", 30) // Check if Alice is equal to a new tuple // with the same structure and values console.log(Equal.equals(alice, Data.tuple("Alice", 30))) // Output: true // Check if Alice is equal to a plain JavaScript tuple // with the same content console.log(Equal.equals(alice, ["Alice", 30])) // Output: false ``` ### array You can use `Data.array` to create an array-like data structure that supports structural equality. **Example** (Creating and Checking Equality of Arrays) ```ts twoslash import { Data, Equal } from "effect" // ┌─── readonly number[] // ▼ const numbers = Data.array([1, 2, 3, 4, 5]) // Check if the array is equal to a new array // with the same values console.log(Equal.equals(numbers, Data.array([1, 2, 3, 4, 5]))) // Output: true // Check if the array is equal to a plain JavaScript array // with the same content console.log(Equal.equals(numbers, [1, 2, 3, 4, 5])) // Output: false ``` ## Constructors The module introduces a concept known as "Case classes", which automate various essential operations when defining data types. These operations include generating **constructors**, handling **equality** checks, and managing **hashing**. Case classes can be defined in two primary ways: - as plain objects using `case` or `tagged` - as TypeScript classes using `Class` or `TaggedClass` ### case The `Data.case` helper generates constructors and built-in support for equality checks and hashing for your data type. **Example** (Defining a Case Class and Checking Equality) In this example, `Data.case` is used to create a constructor for `Person`. The resulting instances have built-in support for equality checks, allowing you to compare them directly using `Equal.equals`. ```ts twoslash import { Data, Equal } from "effect" interface Person { readonly name: string } // Create a constructor for `Person` // // ┌─── (args: { readonly name: string; }) => Person // ▼ const make = Data.case() const alice = make({ name: "Alice" }) console.log(Equal.equals(alice, make({ name: "Alice" }))) // Output: true console.log(Equal.equals(alice, make({ name: "John" }))) // Output: false ``` **Example** (Defining and Comparing Nested Case Classes) This example demonstrates using `Data.case` to create nested data structures, such as a `Person` type containing an `Address`. Both `Person` and `Address` constructors support equality checks. ```ts twoslash import { Data, Equal } from "effect" interface Address { readonly street: string readonly city: string } // Create a constructor for `Address` const Address = Data.case
() interface Person { readonly name: string readonly address: Address } // Create a constructor for `Person` const Person = Data.case() const alice = Person({ name: "Alice", address: Address({ street: "123 Main St", city: "Wonderland" }) }) const anotherAlice = Person({ name: "Alice", address: Address({ street: "123 Main St", city: "Wonderland" }) }) console.log(Equal.equals(alice, anotherAlice)) // Output: true ``` Alternatively, you can use `Data.struct` to create nested data structures without defining a separate `Address` constructor. **Example** (Using `Data.struct` for Nested Objects) ```ts twoslash import { Data, Equal } from "effect" interface Person { readonly name: string readonly address: { readonly street: string readonly city: string } } // Create a constructor for `Person` const Person = Data.case() const alice = Person({ name: "Alice", address: Data.struct({ street: "123 Main St", city: "Wonderland" }) }) const anotherAlice = Person({ name: "Alice", address: Data.struct({ street: "123 Main St", city: "Wonderland" }) }) console.log(Equal.equals(alice, anotherAlice)) // Output: true ``` **Example** (Defining and Comparing Recursive Case Classes) This example demonstrates a recursive structure using `Data.case` to define a binary tree where each node can contain other nodes. ```ts twoslash import { Data, Equal } from "effect" interface BinaryTree { readonly value: T readonly left: BinaryTree | null readonly right: BinaryTree | null } // Create a constructor for `BinaryTree` const BinaryTree = Data.case>() const tree1 = BinaryTree({ value: 0, left: BinaryTree({ value: 1, left: null, right: null }), right: null }) const tree2 = BinaryTree({ value: 0, left: BinaryTree({ value: 1, left: null, right: null }), right: null }) console.log(Equal.equals(tree1, tree2)) // Output: true ``` ### tagged When you're working with a data type that includes a tag field, like in disjoint union types, defining the tag manually for each instance can get repetitive. Using the `case` approach requires you to specify the tag field every time, which can be cumbersome. **Example** (Defining a Tagged Case Class Manually) Here, we create a `Person` type with a `_tag` field using `Data.case`. Notice that the `_tag` needs to be specified for every new instance. ```ts twoslash import { Data } from "effect" interface Person { readonly _tag: "Person" // the tag readonly name: string } const Person = Data.case() // Repeating `_tag: 'Person'` for each instance const alice = Person({ _tag: "Person", name: "Alice" }) const bob = Person({ _tag: "Person", name: "Bob" }) ``` To streamline this process, the `Data.tagged` helper automatically adds the tag. It follows the convention in the Effect ecosystem of naming the tag field as `"_tag"`. **Example** (Using Data.tagged to Simplify Tagging) The `Data.tagged` helper allows you to define the tag just once, making instance creation simpler. ```ts twoslash import { Data } from "effect" interface Person { readonly _tag: "Person" // the tag readonly name: string } const Person = Data.tagged("Person") // The `_tag` field is automatically added const alice = Person({ name: "Alice" }) const bob = Person({ name: "Bob" }) console.log(alice) // Output: { name: 'Alice', _tag: 'Person' } ``` ### Class If you prefer working with classes instead of plain objects, you can use `Data.Class` as an alternative to `Data.case`. This approach may feel more natural in scenarios where you want a class-oriented structure, complete with methods and custom logic. **Example** (Using Data.Class for a Class-Oriented Structure) Here's how to define a `Person` class using `Data.Class`: ```ts twoslash import { Data, Equal } from "effect" // Define a Person class extending Data.Class class Person extends Data.Class<{ name: string }> {} // Create an instance of Person const alice = new Person({ name: "Alice" }) // Check for equality between two instances console.log(Equal.equals(alice, new Person({ name: "Alice" }))) // Output: true ``` One of the benefits of using classes is that you can easily add custom methods and getters. This allows you to extend the functionality of your data types. **Example** (Adding Custom Getters to a Class) In this example, we add a `upperName` getter to the `Person` class to return the name in uppercase: ```ts twoslash import { Data } from "effect" // Extend Person class with a custom getter class Person extends Data.Class<{ name: string }> { get upperName() { return this.name.toUpperCase() } } // Create an instance and use the custom getter const alice = new Person({ name: "Alice" }) console.log(alice.upperName) // Output: ALICE ``` ### TaggedClass If you prefer a class-based approach but also want the benefits of tagging for disjoint unions, `Data.TaggedClass` can be a helpful option. It works similarly to `tagged` but is tailored for class definitions. **Example** (Defining a Tagged Class with Built-In Tagging) Here's how to define a `Person` class using `Data.TaggedClass`. Notice that the tag `"Person"` is automatically added: ```ts twoslash import { Data, Equal } from "effect" // Define a tagged class Person with the _tag "Person" class Person extends Data.TaggedClass("Person")<{ name: string }> {} // Create an instance of Person const alice = new Person({ name: "Alice" }) console.log(alice) // Output: Person { name: 'Alice', _tag: 'Person' } // Check equality between two instances console.log(Equal.equals(alice, new Person({ name: "Alice" }))) // Output: true ``` One benefit of using tagged classes is the ability to easily add custom methods and getters, extending the class's functionality as needed. **Example** (Adding Custom Getters to a Tagged Class) In this example, we add a `upperName` getter to the `Person` class, which returns the name in uppercase: ```ts twoslash import { Data } from "effect" // Extend the Person class with a custom getter class Person extends Data.TaggedClass("Person")<{ name: string }> { get upperName() { return this.name.toUpperCase() } } // Create an instance and use the custom getter const alice = new Person({ name: "Alice" }) console.log(alice.upperName) // Output: ALICE ``` ## Union of Tagged Structs To create a disjoint union of tagged structs, you can use `Data.TaggedEnum` and `Data.taggedEnum`. These utilities make it straightforward to define and work with unions of plain objects. ### Definition The type passed to `Data.TaggedEnum` must be an object where the keys represent the tags, and the values define the structure of the corresponding data types. **Example** (Defining a Tagged Union and Checking Equality) ```ts twoslash import { Data, Equal } from "effect" // Define a union type using TaggedEnum type RemoteData = Data.TaggedEnum<{ Loading: {} Success: { readonly data: string } Failure: { readonly reason: string } }> // Create constructors for each case in the union const { Loading, Success, Failure } = Data.taggedEnum() // Instantiate different states const state1 = Loading() const state2 = Success({ data: "test" }) const state3 = Success({ data: "test" }) const state4 = Failure({ reason: "not found" }) // Check equality between states console.log(Equal.equals(state2, state3)) // Output: true console.log(Equal.equals(state2, state4)) // Output: false // Display the states console.log(state1) // Output: { _tag: 'Loading' } console.log(state2) // Output: { data: 'test', _tag: 'Success' } console.log(state4) // Output: { reason: 'not found', _tag: 'Failure' } ``` ### $is and $match The `Data.taggedEnum` provides `$is` and `$match` functions for convenient type guarding and pattern matching. **Example** (Using Type Guards and Pattern Matching) ```ts twoslash import { Data } from "effect" type RemoteData = Data.TaggedEnum<{ Loading: {} Success: { readonly data: string } Failure: { readonly reason: string } }> const { $is, $match, Loading, Success } = Data.taggedEnum() // Use `$is` to create a type guard for "Loading" const isLoading = $is("Loading") console.log(isLoading(Loading())) // Output: true console.log(isLoading(Success({ data: "test" }))) // Output: false // Use `$match` for pattern matching const matcher = $match({ Loading: () => "this is a Loading", Success: ({ data }) => `this is a Success: ${data}`, Failure: ({ reason }) => `this is a Failre: ${reason}` }) console.log(matcher(Success({ data: "test" }))) // Output: "this is a Success: test" ``` ### Adding Generics You can create more flexible and reusable tagged unions by using `TaggedEnum.WithGenerics`. This approach allows you to define tagged unions that can handle different types dynamically. **Example** (Using Generics with TaggedEnum) ```ts twoslash import { Data } from "effect" // Define a generic TaggedEnum for RemoteData type RemoteData = Data.TaggedEnum<{ Loading: {} Success: { data: Success } Failure: { reason: Failure } }> // Extend TaggedEnum.WithGenerics to add generics interface RemoteDataDefinition extends Data.TaggedEnum.WithGenerics<2> { readonly taggedEnum: RemoteData } // Create constructors for the generic RemoteData const { Loading, Failure, Success } = Data.taggedEnum() // Instantiate each case with specific types const loading = Loading() const failure = Failure({ reason: "not found" }) const success = Success({ data: 1 }) ``` ## Errors In Effect, handling errors is simplified using specialized constructors: - `Error` - `TaggedError` These constructors make defining custom error types straightforward, while also providing useful integrations like equality checks and structured error handling. ### Error `Data.Error` lets you create an `Error` type with extra fields beyond the typical `message` property. **Example** (Creating a Custom Error with Additional Fields) ```ts twoslash import { Data } from "effect" // Define a custom error with additional fields class NotFound extends Data.Error<{ message: string; file: string }> {} // Create an instance of the custom error const err = new NotFound({ message: "Cannot find this file", file: "foo.txt" }) console.log(err instanceof Error) // Output: true console.log(err.file) // Output: foo.txt console.log(err) /* Output: NotFound [Error]: Cannot find this file file: 'foo.txt' ... stack trace ... */ ``` You can yield an instance of `NotFound` directly in an [Effect.gen](/docs/getting-started/using-generators/), without needing to use `Effect.fail`. **Example** (Yielding a Custom Error in `Effect.gen`) ```ts twoslash import { Data, Effect } from "effect" class NotFound extends Data.Error<{ message: string; file: string }> {} const program = Effect.gen(function* () { yield* new NotFound({ message: "Cannot find this file", file: "foo.txt" }) }) Effect.runPromise(program) /* throws: Error: Cannot find this file at ... { name: '(FiberFailure) Error', [Symbol(effect/Runtime/FiberFailure/Cause)]: { _tag: 'Fail', error: NotFound [Error]: Cannot find this file at ...stack trace... file: 'foo.txt' } } } */ ``` ### TaggedError Effect provides a `TaggedError` API to add a `_tag` field automatically to your custom errors. This simplifies error handling with APIs like [Effect.catchTag](/docs/error-management/expected-errors/#catchtag) or [Effect.catchTags](/docs/error-management/expected-errors/#catchtags). ```ts twoslash import { Data, Effect, Console } from "effect" // Define a custom tagged error class NotFound extends Data.TaggedError("NotFound")<{ message: string file: string }> {} const program = Effect.gen(function* () { yield* new NotFound({ message: "Cannot find this file", file: "foo.txt" }) }).pipe( // Catch and handle the tagged error Effect.catchTag("NotFound", (err) => Console.error(`${err.message} (${err.file})`) ) ) Effect.runPromise(program) // Output: Cannot find this file (foo.txt) ``` ### Native Cause Support Errors created using `Data.Error` or `Data.TaggedError` can include a `cause` property, integrating with the native `cause` feature of JavaScript's `Error` for more detailed error tracing. **Example** (Using the `cause` Property) ```ts twoslash {22} import { Data, Effect } from "effect" // Define an error with a cause property class MyError extends Data.Error<{ cause: Error }> {} const program = Effect.gen(function* () { yield* new MyError({ cause: new Error("Something went wrong") }) }) Effect.runPromise(program) /* throws: Error: An error has occurred at ... { name: '(FiberFailure) Error', [Symbol(effect/Runtime/FiberFailure/Cause)]: { _tag: 'Fail', error: MyError at ... [cause]: Error: Something went wrong at ... */ ``` # [DateTime](https://effect.website/docs/data-types/datetime/) ## Overview import { Aside } from "@astrojs/starlight/components" Working with dates and times in JavaScript can be challenging. The built-in `Date` object mutates its internal state, and time zone handling can be confusing. These design choices can lead to errors when working on applications that rely on date-time accuracy, such as scheduling systems, timestamping services, or logging utilities. The DateTime module aims to address these limitations by offering: - **Immutable Data**: Each `DateTime` is an immutable structure, reducing mistakes related to in-place mutations. - **Time Zone Support**: `DateTime` provides robust support for time zones, including automatic daylight saving time adjustments. - **Arithmetic Operations**: You can perform arithmetic operations on `DateTime` instances, such as adding or subtracting durations. ## The DateTime Type A `DateTime` represents a moment in time. It can be stored as either a simple UTC value or as a value with an associated time zone. Storing time this way helps you manage both precise timestamps and the context for how that time should be displayed or interpreted. There are two main variants of `DateTime`: 1. **Utc**: An immutable structure that uses `epochMillis` (milliseconds since the Unix epoch) to represent a point in time in Coordinated Universal Time (UTC). 2. **Zoned**: Includes `epochMillis` along with a `TimeZone`, allowing you to attach an offset or a named region (like "America/New_York") to the timestamp. ### Why Have Two Variants? - **Utc** is straightforward if you only need a universal reference without relying on local time zones. - **Zoned** is helpful when you need to keep track of time zone information for tasks such as converting to local times or adjusting for daylight saving time. ### TimeZone Variants A `TimeZone` can be either: - **Offset**: Represents a fixed offset from UTC (for example, UTC+2 or UTC-5). - **Named**: Uses a named region (e.g., "Europe/London" or "America/New_York") that automatically accounts for region-specific rules like daylight saving time changes. ### TypeScript Definition Below is the TypeScript definition for the `DateTime` type: ```ts showLineNumbers=false type DateTime = Utc | Zoned interface Utc { readonly _tag: "Utc" readonly epochMillis: number } interface Zoned { readonly _tag: "Zoned" readonly epochMillis: number readonly zone: TimeZone } type TimeZone = TimeZone.Offset | TimeZone.Named declare namespace TimeZone { interface Offset { readonly _tag: "Offset" readonly offset: number } interface Named { readonly _tag: "Named" readonly id: string } } ``` ## The DateTime.Parts Type The `DateTime.Parts` type defines the main components of a date, such as the year, month, day, hours, minutes, and seconds. ```ts showLineNumbers=false namespace DateTime { interface Parts { readonly millis: number readonly seconds: number readonly minutes: number readonly hours: number readonly day: number readonly month: number readonly year: number } interface PartsWithWeekday extends Parts { readonly weekDay: number } } ``` ## The DateTime.Input Type The `DateTime.Input` type is a flexible input type that can be used to create a `DateTime` instance. It can be one of the following: - A `DateTime` instance - A JavaScript `Date` object - A numeric value representing milliseconds since the Unix epoch - An object with partial date [parts](#the-datetimeparts-type) (e.g., `{ year: 2024, month: 1, day: 1 }`) - A string that can be parsed by JavaScript's [Date.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse) ```ts showLineNumbers=false namespace DateTime { type Input = DateTime | Partial | Date | number | string } ``` ## Utc Constructors `Utc` is an immutable structure that uses `epochMillis` (milliseconds since the Unix epoch) to represent a point in time in Coordinated Universal Time (UTC). ### unsafeFromDate Creates a `Utc` from a JavaScript `Date`. Throws an `IllegalArgumentException` if the provided `Date` is invalid. When a `Date` object is passed, it is converted to a `Utc` instance. The time is interpreted as the local time of the system executing the code and then adjusted to UTC. This ensures a consistent, timezone-independent representation of the date and time. **Example** (Converting Local Time to UTC in Italy) The following example assumes the code is executed on a system in Italy (CET timezone): ```ts twoslash import { DateTime } from "effect" // Create a Utc instance from a local JavaScript Date // // ┌─── Utc // ▼ const utc = DateTime.unsafeFromDate(new Date("2025-01-01 04:00:00")) console.log(utc) // Output: DateTime.Utc(2025-01-01T03:00:00.000Z) console.log(utc.epochMillis) // Output: 1735700400000 ``` **Explanation**: - The local time **2025-01-01 04:00:00** (in Italy, CET) is converted to **UTC** by subtracting the timezone offset (UTC+1 in January). - As a result, the UTC time becomes **2025-01-01 03:00:00.000Z**. - `epochMillis` provides the same time as milliseconds since the Unix Epoch, ensuring a precise numeric representation of the UTC timestamp. ### unsafeMake Creates a `Utc` from a [DateTime.Input](#the-datetimeinput-type). **Example** (Creating a DateTime with unsafeMake) The following example assumes the code is executed on a system in Italy (CET timezone): ```ts twoslash import { DateTime } from "effect" // From a JavaScript Date const utc1 = DateTime.unsafeMake(new Date("2025-01-01 04:00:00")) console.log(utc1) // Output: DateTime.Utc(2025-01-01T03:00:00.000Z) // From partial date parts const utc2 = DateTime.unsafeMake({ year: 2025 }) console.log(utc2) // Output: DateTime.Utc(2025-01-01T00:00:00.000Z) // From a string const utc3 = DateTime.unsafeMake("2025-01-01") console.log(utc3) // Output: DateTime.Utc(2025-01-01T00:00:00.000Z) ``` **Explanation**: - The local time **2025-01-01 04:00:00** (in Italy, CET) is converted to **UTC** by subtracting the timezone offset (UTC+1 in January). - As a result, the UTC time becomes **2025-01-01 03:00:00.000Z**. ### make Similar to [unsafeMake](#unsafemake), but returns an [Option](/docs/data-types/option/) instead of throwing an error if the input is invalid. If the input is invalid, it returns `None`. If valid, it returns `Some` containing the `Utc`. **Example** (Creating a DateTime Safely) The following example assumes the code is executed on a system in Italy (CET timezone): ```ts twoslash import { DateTime } from "effect" // From a JavaScript Date const maybeUtc1 = DateTime.make(new Date("2025-01-01 04:00:00")) console.log(maybeUtc1) /* Output: { _id: 'Option', _tag: 'Some', value: '2025-01-01T03:00:00.000Z' } */ // From partial date parts const maybeUtc2 = DateTime.make({ year: 2025 }) console.log(maybeUtc2) /* Output: { _id: 'Option', _tag: 'Some', value: '2025-01-01T00:00:00.000Z' } */ // From a string const maybeUtc3 = DateTime.make("2025-01-01") console.log(maybeUtc3) /* Output: { _id: 'Option', _tag: 'Some', value: '2025-01-01T00:00:00.000Z' } */ ``` **Explanation**: - The local time **2025-01-01 04:00:00** (in Italy, CET) is converted to **UTC** by subtracting the timezone offset (UTC+1 in January). - As a result, the UTC time becomes **2025-01-01 03:00:00.000Z**. ## Zoned Constructors A `Zoned` includes `epochMillis` along with a `TimeZone`, allowing you to attach an offset or a named region (like "America/New_York") to the timestamp. ### unsafeMakeZoned Creates a `Zoned` by combining a [DateTime.Input](#the-datetimeinput-type) with an optional `TimeZone`. This allows you to represent a specific point in time with an associated time zone. The time zone can be provided in several ways: - As a `TimeZone` object - A string identifier (e.g., `"Europe/London"`) - A numeric offset in milliseconds If the input or time zone is invalid, an `IllegalArgumentException` is thrown. **Example** (Creating a Zoned DateTime Without Specifying a Time Zone) The following example assumes the code is executed on a system in Italy (CET timezone): ```ts twoslash import { DateTime } from "effect" // Create a Zoned DateTime based on the system's local time zone const zoned = DateTime.unsafeMakeZoned(new Date("2025-01-01 04:00:00")) console.log(zoned) // Output: DateTime.Zoned(2025-01-01T04:00:00.000+01:00) console.log(zoned.zone) // Output: TimeZone.Offset(+01:00) ``` Here, the system's time zone (CET, which is UTC+1 in January) is used to create the `Zoned` instance. **Example** (Specifying a Named Time Zone) The following example assumes the code is executed on a system in Italy (CET timezone): ```ts twoslash import { DateTime } from "effect" // Create a Zoned DateTime with a specified named time zone const zoned = DateTime.unsafeMakeZoned(new Date("2025-01-01 04:00:00"), { timeZone: "Europe/Rome" }) console.log(zoned) // Output: DateTime.Zoned(2025-01-01T04:00:00.000+01:00[Europe/Rome]) console.log(zoned.zone) // Output: TimeZone.Named(Europe/Rome) ``` In this case, the `"Europe/Rome"` time zone is explicitly provided, resulting in the `Zoned` instance being tied to this named time zone. By default, the input date is treated as a UTC value and then adjusted for the specified time zone. To interpret the input date as being in the specified time zone, you can use the `adjustForTimeZone` option. **Example** (Adjusting for Time Zone Interpretation) The following example assumes the code is executed on a system in Italy (CET timezone): ```ts twoslash import { DateTime } from "effect" // Interpret the input date as being in the specified time zone const zoned = DateTime.unsafeMakeZoned(new Date("2025-01-01 04:00:00"), { timeZone: "Europe/Rome", adjustForTimeZone: true }) console.log(zoned) // Output: DateTime.Zoned(2025-01-01T03:00:00.000+01:00[Europe/Rome]) console.log(zoned.zone) // Output: TimeZone.Named(Europe/Rome) ``` **Explanation** - **Without `adjustForTimeZone`**: The input date is interpreted as UTC and then adjusted to the specified time zone. For instance, `2025-01-01 04:00:00` in UTC becomes `2025-01-01T04:00:00.000+01:00` in CET (UTC+1). - **With `adjustForTimeZone: true`**: The input date is interpreted as being in the specified time zone. For example, `2025-01-01 04:00:00` in "Europe/Rome" (CET) is adjusted to its corresponding UTC time, resulting in `2025-01-01T03:00:00.000+01:00`. ### makeZoned The `makeZoned` function works similarly to [unsafeMakeZoned](#unsafemakezoned) but provides a safer approach. Instead of throwing an error when the input is invalid, it returns an `Option`. If the input is invalid, it returns `None`. If valid, it returns `Some` containing the `Zoned`. **Example** (Safely Creating a Zoned DateTime) ```ts twoslash import { DateTime, Option } from "effect" // ┌─── Option // ▼ const zoned = DateTime.makeZoned(new Date("2025-01-01 04:00:00"), { timeZone: "Europe/Rome" }) if (Option.isSome(zoned)) { console.log("The DateTime is valid") } ``` ### makeZonedFromString Creates a `Zoned` by parsing a string in the format `YYYY-MM-DDTHH:mm:ss.sss+HH:MM[IANA timezone identifier]`. If the input string is valid, the function returns a `Some` containing the `Zoned`. If the input is invalid, it returns `None`. **Example** (Parsing a Zoned DateTime from a String) ```ts twoslash import { DateTime, Option } from "effect" // ┌─── Option // ▼ const zoned = DateTime.makeZonedFromString( "2025-01-01T03:00:00.000+01:00[Europe/Rome]" ) if (Option.isSome(zoned)) { console.log("The DateTime is valid") } ``` ## Current Time ### now Provides the current UTC time as a `Effect`, using the [Clock](/docs/requirements-management/default-services/) service. **Example** (Retrieving the Current UTC Time) ```ts twoslash import { DateTime, Effect } from "effect" const program = Effect.gen(function* () { // ┌─── Utc // ▼ const currentTime = yield* DateTime.now }) ``` ### unsafeNow Retrieves the current UTC time immediately using `Date.now()`, without the [Clock](/docs/requirements-management/default-services/) service. **Example** (Getting the Current UTC Time Immediately) ```ts twoslash import { DateTime } from "effect" // ┌─── Utc // ▼ const currentTime = DateTime.unsafeNow() ``` ## Guards | Function | Description | | ------------------ | ---------------------------------------------- | | `isDateTime` | Checks if a value is a `DateTime`. | | `isTimeZone` | Checks if a value is a `TimeZone`. | | `isTimeZoneOffset` | Checks if a value is a `TimeZone.Offset`. | | `isTimeZoneNamed` | Checks if a value is a `TimeZone.Named`. | | `isUtc` | Checks if a `DateTime` is the `Utc` variant. | | `isZoned` | Checks if a `DateTime` is the `Zoned` variant. | **Example** (Validating a DateTime) ```ts twoslash import { DateTime } from "effect" function printDateTimeInfo(x: unknown) { if (DateTime.isDateTime(x)) { console.log("This is a valid DateTime") } else { console.log("Not a DateTime") } } ``` ## Time Zone Management | Function | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `setZone` | Creates a `Zoned` from `DateTime` by applying the given `TimeZone`. | | `setZoneOffset` | Creates a `Zoned` from `DateTime` using a fixed offset (in ms). | | `setZoneNamed` | Creates a `Zoned` from `DateTime` from an IANA time zone identifier or returns `None` if invalid. | | `unsafeSetZoneNamed` | Creates a `Zoned` from `DateTime` from an IANA time zone identifier or throws if invalid. | | `zoneUnsafeMakeNamed` | Creates a `TimeZone.Named` from a IANA time zone identifier or throws if the identifier is invalid. | | `zoneMakeNamed` | Creates a `TimeZone.Named` from a IANA time zone identifier or returns `None` if invalid. | | `zoneMakeNamedEffect` | Creates a `Effect` from a IANA time zone identifier failing with `IllegalArgumentException` if invalid | | `zoneMakeOffset` | Creates a `TimeZone.Offset` from a numeric offset in milliseconds. | | `zoneMakeLocal` | Creates a `TimeZone.Named` from the system's local time zone. | | `zoneFromString` | Attempts to parse a time zone from a string, returning `None` if invalid. | | `zoneToString` | Returns a string representation of a `TimeZone`. | **Example** (Applying a Time Zone to a DateTime) ```ts twoslash import { DateTime } from "effect" // Create a UTC DateTime // // ┌─── Utc // ▼ const utc = DateTime.unsafeMake("2024-01-01") // Create a named time zone for New York // // ┌─── TimeZone.Named // ▼ const zoneNY = DateTime.zoneUnsafeMakeNamed("America/New_York") // Apply it to the DateTime // // ┌─── Zoned // ▼ const zoned = DateTime.setZone(utc, zoneNY) console.log(zoned) // Output: DateTime.Zoned(2023-12-31T19:00:00.000-05:00[America/New_York]) ``` ### zoneFromString Parses a string to create a `DateTime.TimeZone`. This function attempts to interpret the input string as either: - A numeric time zone offset (e.g., "GMT", "+01:00") - An IANA time zone identifier (e.g., "Europe/London") If the string matches an offset format, it is converted into a `TimeZone.Offset`. Otherwise, it attempts to create a `TimeZone.Named` using the input. If the input string is invalid, `Option.none()` is returned. **Example** (Parsing a Time Zone from a String) ```ts twoslash import { DateTime, Option } from "effect" // Attempt to parse a numeric offset const offsetZone = DateTime.zoneFromString("+01:00") console.log(Option.isSome(offsetZone)) // Output: true // Attempt to parse an IANA time zone const namedZone = DateTime.zoneFromString("Europe/London") console.log(Option.isSome(namedZone)) // Output: true // Invalid input const invalidZone = DateTime.zoneFromString("Invalid/Zone") console.log(Option.isSome(invalidZone)) // Output: false ``` ## Comparisons | Function | Description | | -------------------------------------------- | ------------------------------------------------------------ | | `distance` | Returns the difference (in ms) between two `DateTime`s. | | `distanceDurationEither` | Returns a `Left` or `Right` `Duration` depending on order. | | `distanceDuration` | Returns a `Duration` indicating how far apart two times are. | | `min` | Returns the earlier of two `DateTime` values. | | `max` | Returns the later of two `DateTime` values. | | `greaterThan`, `greaterThanOrEqualTo`, etc. | Checks ordering between two `DateTime` values. | | `between` | Checks if a `DateTime` lies within the given bounds. | | `isFuture`, `isPast`, `unsafeIsFuture`, etc. | Checks if a `DateTime` is in the future or past. | **Example** (Finding the Distance Between Two DateTimes) ```ts twoslash import { DateTime } from "effect" const utc1 = DateTime.unsafeMake("2025-01-01T00:00:00Z") const utc2 = DateTime.add(utc1, { days: 1 }) console.log(DateTime.distance(utc1, utc2)) // Output: 86400000 (one day) console.log(DateTime.distanceDurationEither(utc1, utc2)) /* Output: { _id: 'Either', _tag: 'Right', right: { _id: 'Duration', _tag: 'Millis', millis: 86400000 } } */ console.log(DateTime.distanceDuration(utc1, utc2)) // Output: { _id: 'Duration', _tag: 'Millis', millis: 86400000 } ``` ## Conversions | Function | Description | | ---------------- | ----------------------------------------------------------------------- | | `toDateUtc` | Returns a JavaScript `Date` in UTC. | | `toDate` | Applies the time zone (if present) and converts to a JavaScript `Date`. | | `zonedOffset` | For a `Zoned` DateTime, returns the time zone offset in ms. | | `zonedOffsetIso` | For a `Zoned` DateTime, returns an ISO offset string like "+01:00". | | `toEpochMillis` | Returns the Unix epoch time in milliseconds. | | `removeTime` | Returns a `Utc` with the time cleared (only date remains). | ## Parts | Function | Description | | -------------------------- | ---------------------------------------------------------------------- | | `toParts` | Returns time zone adjusted date parts (including weekday). | | `toPartsUtc` | Returns UTC date parts (including weekday). | | `getPart` / `getPartUtc` | Retrieves a specific part (e.g., `"year"` or `"month"`) from the date. | | `setParts` / `setPartsUtc` | Updates certain parts of a date, preserving or ignoring the time zone. | **Example** (Extracting Parts from a DateTime) ```ts twoslash import { DateTime } from "effect" const zoned = DateTime.setZone( DateTime.unsafeMake("2024-01-01"), DateTime.zoneUnsafeMakeNamed("Europe/Rome") ) console.log(DateTime.getPart(zoned, "month")) // Output: 1 ``` ## Math | Function | Description | | ------------------ | ------------------------------------------------------------------------------------------ | | `addDuration` | Adds the given `Duration` to a `DateTime`. | | `subtractDuration` | Subtracts the given `Duration` from a `DateTime`. | | `add` | Adds numeric parts (e.g., `{ hours: 2 }`) to a `DateTime`. | | `subtract` | Subtracts numeric parts. | | `startOf` | Moves a `DateTime` to the start of the given unit (e.g., the beginning of a day or month). | | `endOf` | Moves a `DateTime` to the end of the given unit. | | `nearest` | Rounds a `DateTime` to the nearest specified unit. | ## Formatting | Function | Description | | ------------------ | -------------------------------------------------------------------- | | `format` | Formats a `DateTime` as a string using the `DateTimeFormat` API. | | `formatLocal` | Uses the system's local time zone and locale for formatting. | | `formatUtc` | Forces UTC formatting. | | `formatIntl` | Uses a provided `Intl.DateTimeFormat`. | | `formatIso` | Returns an ISO 8601 string in UTC. | | `formatIsoDate` | Returns an ISO date string, adjusted for the time zone. | | `formatIsoDateUtc` | Returns an ISO date string in UTC. | | `formatIsoOffset` | Formats a `Zoned` as a string with an offset like "+01:00". | | `formatIsoZoned` | Formats a `Zoned` in the form `YYYY-MM-DDTHH:mm:ss.sss+HH:MM[Zone]`. | ## Layers for Current Time Zone | Function | Description | | ------------------------ | -------------------------------------------------------------------- | | `CurrentTimeZone` | A service tag for the current time zone. | | `setZoneCurrent` | Sets a `DateTime` to use the current time zone. | | `withCurrentZone` | Provides an effect with a specified time zone. | | `withCurrentZoneLocal` | Uses the system's local time zone for the effect. | | `withCurrentZoneOffset` | Uses a fixed offset (in ms) for the effect. | | `withCurrentZoneNamed` | Uses a named time zone identifier (e.g., "Europe/London"). | | `nowInCurrentZone` | Retrieves the current time as a `Zoned` in the configured time zone. | | `layerCurrentZone` | Creates a Layer providing the `CurrentTimeZone` service. | | `layerCurrentZoneOffset` | Creates a Layer from a fixed offset. | | `layerCurrentZoneNamed` | Creates a Layer from a named time zone, failing if invalid. | | `layerCurrentZoneLocal` | Creates a Layer from the system's local time zone. | **Example** (Using the Current Time Zone in an Effect) ```ts twoslash import { DateTime, Effect } from "effect" // Retrieve the current time in the "Europe/London" time zone const program = Effect.gen(function* () { const zonedNow = yield* DateTime.nowInCurrentZone console.log(zonedNow) }).pipe(DateTime.withCurrentZoneNamed("Europe/London")) Effect.runFork(program) /* Example Output: DateTime.Zoned(2025-01-06T18:36:38.573+00:00[Europe/London]) */ ``` # [Duration](https://effect.website/docs/data-types/duration/) ## Overview The `Duration` data type data type is used to represent specific non-negative spans of time. It is commonly used to represent time intervals or durations in various operations, such as timeouts, delays, or scheduling. The `Duration` type provides a convenient way to work with time units and perform calculations on durations. ## Creating Durations The Duration module includes several constructors to create durations in different units. **Example** (Creating Durations in Various Units) ```ts twoslash import { Duration } from "effect" // Create a duration of 100 milliseconds const duration1 = Duration.millis(100) // Create a duration of 2 seconds const duration2 = Duration.seconds(2) // Create a duration of 5 minutes const duration3 = Duration.minutes(5) ``` You can create durations using units such as nanoseconds, microsecond, milliseconds, seconds, minutes, hours, days, and weeks. For an infinite duration, use `Duration.infinity`. **Example** (Creating an Infinite Duration) ```ts twoslash import { Duration } from "effect" console.log(String(Duration.infinity)) /* Output: Duration(Infinity) */ ``` Another option for creating durations is using the `Duration.decode` helper: - `number` values are treated as milliseconds. - `bigint` values are treated as nanoseconds. - Strings must follow the format `"${number} ${unit}"`. **Example** (Decoding Values into Durations) ```ts twoslash import { Duration } from "effect" Duration.decode(10n) // same as Duration.nanos(10) Duration.decode(100) // same as Duration.millis(100) Duration.decode(Infinity) // same as Duration.infinity Duration.decode("10 nanos") // same as Duration.nanos(10) Duration.decode("20 micros") // same as Duration.micros(20) Duration.decode("100 millis") // same as Duration.millis(100) Duration.decode("2 seconds") // same as Duration.seconds(2) Duration.decode("5 minutes") // same as Duration.minutes(5) Duration.decode("7 hours") // same as Duration.hours(7) Duration.decode("3 weeks") // same as Duration.weeks(3) ``` ## Getting the Duration Value You can retrieve the value of a duration in milliseconds using `Duration.toMillis`. **Example** (Getting Duration in Milliseconds) ```ts twoslash import { Duration } from "effect" console.log(Duration.toMillis(Duration.seconds(30))) // Output: 30000 ``` To get the value of a duration in nanoseconds, use `Duration.toNanos`. Note that `toNanos` returns an `Option` because the duration might be infinite. **Example** (Getting Duration in Nanoseconds) ```ts twoslash import { Duration } from "effect" console.log(Duration.toNanos(Duration.millis(100))) /* Output: { _id: 'Option', _tag: 'Some', value: 100000000n } */ ``` To get a `bigint` value without `Option`, use `Duration.unsafeToNanos`. However, it will throw an error for infinite durations. **Example** (Retrieving Nanoseconds Unsafely) ```ts twoslash import { Duration } from "effect" console.log(Duration.unsafeToNanos(Duration.millis(100))) // Output: 100000000n console.log(Duration.unsafeToNanos(Duration.infinity)) /* throws: Error: Cannot convert infinite duration to nanos ...stack trace... */ ``` ## Comparing Durations Use the following functions to compare two durations: | API | Description | | ---------------------- | ---------------------------------------------------------------------------- | | `lessThan` | Returns `true` if the first duration is less than the second. | | `lessThanOrEqualTo` | Returns `true` if the first duration is less than or equal to the second. | | `greaterThan` | Returns `true` if the first duration is greater than the second. | | `greaterThanOrEqualTo` | Returns `true` if the first duration is greater than or equal to the second. | **Example** (Comparing Two Durations) ```ts twoslash import { Duration } from "effect" const duration1 = Duration.seconds(30) const duration2 = Duration.minutes(1) console.log(Duration.lessThan(duration1, duration2)) // Output: true console.log(Duration.lessThanOrEqualTo(duration1, duration2)) // Output: true console.log(Duration.greaterThan(duration1, duration2)) // Output: false console.log(Duration.greaterThanOrEqualTo(duration1, duration2)) // Output: false ``` ## Performing Arithmetic Operations You can perform arithmetic operations on durations, like addition and multiplication. **Example** (Adding and Multiplying Durations) ```ts twoslash import { Duration } from "effect" const duration1 = Duration.seconds(30) const duration2 = Duration.minutes(1) // Add two durations console.log(String(Duration.sum(duration1, duration2))) /* Output: Duration(1m 30s) */ // Multiply a duration by a factor console.log(String(Duration.times(duration1, 2))) /* Output: Duration(1m) */ ``` # [Either](https://effect.website/docs/data-types/either/) ## Overview import { Aside } from "@astrojs/starlight/components" The `Either` data type represents two exclusive values: an `Either` can be a `Right` value or a `Left` value, where `R` is the type of the `Right` value, and `L` is the type of the `Left` value. ## Understanding Either and Exit Either is primarily used as a **simple discriminated union** and is not recommended as the main result type for operations requiring detailed error information. [Exit](/docs/data-types/exit/) is the preferred **result type** within Effect for capturing comprehensive details about failures. It encapsulates the outcomes of effectful computations, distinguishing between success and various failure modes, such as errors, defects and interruptions. ## Creating Eithers You can create an `Either` using the `Either.right` and `Either.left` constructors. Use `Either.right` to create a `Right` value of type `R`. **Example** (Creating a Right Value) ```ts twoslash import { Either } from "effect" const rightValue = Either.right(42) console.log(rightValue) /* Output: { _id: 'Either', _tag: 'Right', right: 42 } */ ``` Use `Either.left` to create a `Left` value of type `L`. **Example** (Creating a Left Value) ```ts twoslash import { Either } from "effect" const leftValue = Either.left("not a number") console.log(leftValue) /* Output: { _id: 'Either', _tag: 'Left', left: 'not a number' } */ ``` ## Guards Use `Either.isLeft` and `Either.isRight` to check whether an `Either` is a `Left` or `Right` value. **Example** (Using Guards to Check the Type of Either) ```ts twoslash import { Either } from "effect" const foo = Either.right(42) if (Either.isLeft(foo)) { console.log(`The left value is: ${foo.left}`) } else { console.log(`The Right value is: ${foo.right}`) } // Output: "The Right value is: 42" ``` ## Pattern Matching Use `Either.match` to handle both cases of an `Either` by specifying separate callbacks for `Left` and `Right`. **Example** (Pattern Matching with Either) ```ts twoslash import { Either } from "effect" const foo = Either.right(42) const message = Either.match(foo, { onLeft: (left) => `The left value is: ${left}`, onRight: (right) => `The Right value is: ${right}` }) console.log(message) // Output: "The Right value is: 42" ``` ## Mapping ### Mapping over the Right Value Use `Either.map` to transform the `Right` value of an `Either`. The function you provide will only apply to the `Right` value, leaving any `Left` value unchanged. **Example** (Transforming the Right Value) ```ts twoslash import { Either } from "effect" // Transform the Right value by adding 1 const rightResult = Either.map(Either.right(1), (n) => n + 1) console.log(rightResult) /* Output: { _id: 'Either', _tag: 'Right', right: 2 } */ // The transformation is ignored for Left values const leftResult = Either.map(Either.left("not a number"), (n) => n + 1) console.log(leftResult) /* Output: { _id: 'Either', _tag: 'Left', left: 'not a number' } */ ``` ### Mapping over the Left Value Use `Either.mapLeft` to transform the `Left` value of an `Either`. The provided function only applies to the `Left` value, leaving any `Right` value unchanged. **Example** (Transforming the Left Value) ```ts twoslash import { Either } from "effect" // The transformation is ignored for Right values const rightResult = Either.mapLeft(Either.right(1), (s) => s + "!") console.log(rightResult) /* Output: { _id: 'Either', _tag: 'Right', right: 1 } */ // Transform the Left value by appending "!" const leftResult = Either.mapLeft( Either.left("not a number"), (s) => s + "!" ) console.log(leftResult) /* Output: { _id: 'Either', _tag: 'Left', left: 'not a number!' } */ ``` ### Mapping over Both Values Use `Either.mapBoth` to transform both the `Left` and `Right` values of an `Either`. This function takes two separate transformation functions: one for the `Left` value and another for the `Right` value. **Example** (Transforming Both Left and Right Values) ```ts twoslash import { Either } from "effect" const transformedRight = Either.mapBoth(Either.right(1), { onLeft: (s) => s + "!", onRight: (n) => n + 1 }) console.log(transformedRight) /* Output: { _id: 'Either', _tag: 'Right', right: 2 } */ const transformedLeft = Either.mapBoth(Either.left("not a number"), { onLeft: (s) => s + "!", onRight: (n) => n + 1 }) console.log(transformedLeft) /* Output: { _id: 'Either', _tag: 'Left', left: 'not a number!' } */ ``` ## Interop with Effect The `Either` type works as a subtype of the `Effect` type, allowing you to use it with functions from the `Effect` module. While these functions are built to handle `Effect` values, they can also manage `Either` values correctly. ### How Either Maps to Effect | Either Variant | Mapped to Effect | Description | | -------------- | ------------------ | -------------------- | | `Left` | `Effect` | Represents a failure | | `Right` | `Effect` | Represents a success | **Example** (Combining `Either` with `Effect`) ```ts twoslash import { Effect, Either } from "effect" // Function to get the head of an array, returning Either const head = (array: ReadonlyArray): Either.Either => array.length > 0 ? Either.right(array[0]) : Either.left("empty array") // Simulated fetch function that returns Effect const fetchData = (): Effect.Effect => { const success = Math.random() > 0.5 return success ? Effect.succeed("some data") : Effect.fail("Failed to fetch data") } // Mixing Either and Effect const program = Effect.all([head([1, 2, 3]), fetchData()]) Effect.runPromise(program).then(console.log) /* Example Output: [ 1, 'some data' ] */ ``` ## Combining Two or More Eithers ### zipWith The `Either.zipWith` function lets you combine two `Either` values using a provided function. It creates a new `Either` that holds the combined value of both original `Either` values. **Example** (Combining Two Eithers into an Object) ```ts twoslash import { Either } from "effect" const maybeName: Either.Either = Either.right("John") const maybeAge: Either.Either = Either.right(25) // Combine the name and age into a person object const person = Either.zipWith(maybeName, maybeAge, (name, age) => ({ name: name.toUpperCase(), age })) console.log(person) /* Output: { _id: 'Either', _tag: 'Right', right: { name: 'JOHN', age: 25 } } */ ``` If either of the `Either` values is `Left`, the result will be `Left`, holding the first encountered `Left` value: **Example** (Combining Eithers with a Left Value) ```ts twoslash {4} import { Either } from "effect" const maybeName: Either.Either = Either.right("John") const maybeAge: Either.Either = Either.left("Oh no!") // Since maybeAge is a Left, the result will also be Left const person = Either.zipWith(maybeName, maybeAge, (name, age) => ({ name: name.toUpperCase(), age })) console.log(person) /* Output: { _id: 'Either', _tag: 'Left', left: 'Oh no!' } */ ``` ### all To combine multiple `Either` values without transforming their contents, you can use `Either.all`. This function returns an `Either` with a structure matching the input: - If you pass a tuple, the result will be a tuple of the same length. - If you pass a struct, the result will be a struct with the same keys. - If you pass an `Iterable`, the result will be an array. **Example** (Combining Multiple Eithers into a Tuple and Struct) ```ts twoslash import { Either } from "effect" const maybeName: Either.Either = Either.right("John") const maybeAge: Either.Either = Either.right(25) // ┌─── Either<[string, number], string> // ▼ const tuple = Either.all([maybeName, maybeAge]) console.log(tuple) /* Output: { _id: 'Either', _tag: 'Right', right: [ 'John', 25 ] } */ // ┌─── Either<{ name: string; age: number; }, string> // ▼ const struct = Either.all({ name: maybeName, age: maybeAge }) console.log(struct) /* Output: { _id: 'Either', _tag: 'Right', right: { name: 'John', age: 25 } } */ ``` If one or more `Either` values are `Left`, the first `Left` encountered is returned: **Example** (Handling Multiple Left Values) ```ts import { Either } from "effect" const maybeName: Either.Either = Either.left("name not found") const maybeAge: Either.Either = Either.left("age not found") // The first Left value will be returned console.log(Either.all([maybeName, maybeAge])) /* Output: { _id: 'Either', _tag: 'Left', left: 'name not found' } */ ``` ## gen Similar to [Effect.gen](/docs/getting-started/using-generators/), `Either.gen` provides a more readable, generator-based syntax for working with `Either` values, making code that involves `Either` easier to write and understand. This approach is similar to using `async/await` but tailored for `Either`. **Example** (Using `Either.gen` to Create a Combined Value) ```ts twoslash import { Either } from "effect" const maybeName: Either.Either = Either.right("John") const maybeAge: Either.Either = Either.right(25) const program = Either.gen(function* () { const name = (yield* maybeName).toUpperCase() const age = yield* maybeAge return { name, age } }) console.log(program) /* Output: { _id: 'Either', _tag: 'Right', right: { name: 'JOHN', age: 25 } } */ ``` When any of the `Either` values in the sequence is `Left`, the generator immediately returns the `Left` value, skipping further operations: **Example** (Handling a `Left` Value with `Either.gen`) In this example, `Either.gen` halts execution as soon as it encounters the `Left` value, effectively propagating the error without performing further operations. ```ts twoslash import { Either } from "effect" const maybeName: Either.Either = Either.left("Oh no!") const maybeAge: Either.Either = Either.right(25) const program = Either.gen(function* () { console.log("Retrieving name...") const name = (yield* maybeName).toUpperCase() console.log("Retrieving age...") const age = yield* maybeAge return { name, age } }) console.log(program) /* Output: Retrieving name... { _id: 'Either', _tag: 'Left', left: 'Oh no!' } */ ``` The use of `console.log` in these example is for demonstration purposes only. When using `Either.gen`, avoid including side effects in your generator functions, as `Either` should remain a pure data structure. # [Exit](https://effect.website/docs/data-types/exit/) ## Overview An `Exit` describes the result of running an `Effect` workflow. There are two possible states for an `Exit`: - `Exit.Success`: Contains a success value of type `A`. - `Exit.Failure`: Contains a failure [Cause](/docs/data-types/cause/) of type `E`. ## Creating Exits The Exit module provides two primary functions for constructing exit values: `Exit.succeed` and `Exit.fail`. These functions represent the outcomes of an effectful computation in terms of success or failure. ### succeed `Exit.succeed` creates an `Exit` value that represents a successful outcome. You use this function when you want to indicate that a computation completed successfully and to provide the resulting value. **Example** (Creating a Successful Exit) ```ts twoslash import { Exit } from "effect" // Create an Exit representing a successful outcome with the value 42 const successExit = Exit.succeed(42) console.log(successExit) // Output: { _id: 'Exit', _tag: 'Success', value: 42 } ``` ### fail `Exit.fail` creates an `Exit` value that represents a failure. The failure is described using a [Cause](/docs/data-types/cause/) object, which can encapsulate expected errors, defects, interruptions, or even composite errors. **Example** (Creating a Failed Exit) ```ts twoslash import { Exit, Cause } from "effect" // Create an Exit representing a failure with an error message const failureExit = Exit.fail(Cause.fail("Something went wrong")) console.log(failureExit) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Something went wrong' } } */ ``` ## Pattern Matching You can handle different outcomes of an `Exit` using the `Exit.match` function. This function lets you provide two separate callbacks to handle both success and failure cases of an `Effect` execution. **Example** (Matching Success and Failure States) ```ts twoslash import { Effect, Exit, Cause } from "effect" const simulatedSuccess = Effect.runSyncExit(Effect.succeed(1)) console.log( Exit.match(simulatedSuccess, { onFailure: (cause) => `Exited with failure state: ${Cause.pretty(cause)}`, onSuccess: (value) => `Exited with success value: ${value}` }) ) // Output: "Exited with success value: 1" const simulatedFailure = Effect.runSyncExit(Effect.fail("error")) console.log( Exit.match(simulatedFailure, { onFailure: (cause) => `Exited with failure state: ${Cause.pretty(cause)}`, onSuccess: (value) => `Exited with success value: ${value}` }) ) // Output: "Exited with failure state: Error: error" ``` ## Exit vs Either Conceptually, `Exit` can be thought of as `Either>`. However, the [Cause](/docs/data-types/cause/) type represents more than just expected errors of type `E`. It includes: - Interruption causes - Defects (unexpected errors) - The combination of multiple causes This allows `Cause` to capture richer and more complex error states compared to a simple `Either`. ## Exit vs Effect `Exit` is actually a subtype of `Effect`. This means that `Exit` values can also be considered as `Effect` values. - An `Exit`, in essence, is a "constant computation". - `Effect.succeed` is essentially the same as `Exit.succeed`. - `Effect.fail` is the same as `Exit.fail`. # [Option](https://effect.website/docs/data-types/option/) ## Overview import { Aside } from "@astrojs/starlight/components" The `Option` data type represents optional values. An `Option` can either be `Some`, containing a value of type `A`, or `None`, representing the absence of a value. You can use `Option` in scenarios like: - Using it for initial values - Returning values from functions that are not defined for all possible inputs (referred to as "partial functions") - Managing optional fields in data structures - Handling optional function arguments ## Creating Options ### some Use the `Option.some` constructor to create an `Option` that holds a value of type `A`. **Example** (Creating an Option with a Value) ```ts twoslash import { Option } from "effect" // An Option holding the number 1 const value = Option.some(1) console.log(value) // Output: { _id: 'Option', _tag: 'Some', value: 1 } ``` ### none Use the `Option.none` constructor to create an `Option` representing the absence of a value. **Example** (Creating an Option with No Value) ```ts twoslash import { Option } from "effect" // An Option holding no value const noValue = Option.none() console.log(noValue) // Output: { _id: 'Option', _tag: 'None' } ``` ### liftPredicate You can create an `Option` based on a predicate, for example, to check if a value is positive. **Example** (Using Explicit Option Creation) Here's how you can achieve this using `Option.none` and `Option.some`: ```ts twoslash import { Option } from "effect" const isPositive = (n: number) => n > 0 const parsePositive = (n: number): Option.Option => isPositive(n) ? Option.some(n) : Option.none() ``` **Example** (Using `Option.liftPredicate` for Conciseness) Alternatively, you can simplify the above logic with `Option.liftPredicate`: ```ts twoslash import { Option } from "effect" const isPositive = (n: number) => n > 0 // ┌─── (b: number) => Option // ▼ const parsePositive = Option.liftPredicate(isPositive) ``` ## Modeling Optional Properties Consider a `User` model where the `"email"` property is optional and can hold a `string` value. We use the `Option` type to represent this optional property: ```ts {6} twoslash import { Option } from "effect" interface User { readonly id: number readonly username: string readonly email: Option.Option } ``` Here are examples of how to create `User` instances with and without an email: **Example** (Creating Users with and without Email) ```ts twoslash import { Option } from "effect" interface User { readonly id: number readonly username: string readonly email: Option.Option } const withEmail: User = { id: 1, username: "john_doe", email: Option.some("john.doe@example.com") } const withoutEmail: User = { id: 2, username: "jane_doe", email: Option.none() } ``` ## Guards You can check whether an `Option` is a `Some` or a `None` using the `Option.isSome` and `Option.isNone` guards. **Example** (Using Guards to Check Option Values) ```ts twoslash import { Option } from "effect" const foo = Option.some(1) console.log(Option.isSome(foo)) // Output: true if (Option.isNone(foo)) { console.log("Option is empty") } else { console.log(`Option has a value: ${foo.value}`) } // Output: "Option has a value: 1" ``` ## Pattern Matching Use `Option.match` to handle both cases of an `Option` by specifying separate callbacks for `None` and `Some`. **Example** (Pattern Matching with Option) ```ts twoslash import { Option } from "effect" const foo = Option.some(1) const message = Option.match(foo, { onNone: () => "Option is empty", onSome: (value) => `Option has a value: ${value}` }) console.log(message) // Output: "Option has a value: 1" ``` ## Working with Option ### map The `Option.map` function lets you transform the value inside an `Option` without manually unwrapping and re-wrapping it. If the `Option` holds a value (`Some`), the transformation function is applied. If the `Option` is `None`, the function is ignored, and the `Option` remains unchanged. **Example** (Mapping a Value in Some) ```ts twoslash import { Option } from "effect" // Transform the value inside Some console.log(Option.map(Option.some(1), (n) => n + 1)) // Output: { _id: 'Option', _tag: 'Some', value: 2 } ``` When dealing with `None`, the mapping function is not executed, and the `Option` remains `None`: **Example** (Mapping over None) ```ts twoslash import { Option } from "effect" // Mapping over None results in None console.log(Option.map(Option.none(), (n) => n + 1)) // Output: { _id: 'Option', _tag: 'None' } ``` ### flatMap The `Option.flatMap` function is similar to `Option.map`, but it is designed to handle cases where the transformation might return another `Option`. This allows us to chain computations that depend on whether or not a value is present in an `Option`. Consider a `User` model that includes a nested optional `Address`, which itself contains an optional `street` property: ```ts twoslash {7,12} import { Option } from "effect" interface User { readonly id: number readonly username: string readonly email: Option.Option readonly address: Option.Option
} interface Address { readonly city: string readonly street: Option.Option } ``` In this model, the `address` field is an `Option
`, and the `street` field within `Address` is an `Option`. We can use `Option.flatMap` to extract the `street` property from `address`: **Example** (Extracting a Nested Optional Property) ```ts twoslash import { Option } from "effect" interface Address { readonly city: string readonly street: Option.Option } interface User { readonly id: number readonly username: string readonly email: Option.Option readonly address: Option.Option
} const user: User = { id: 1, username: "john_doe", email: Option.some("john.doe@example.com"), address: Option.some({ city: "New York", street: Option.some("123 Main St") }) } // Use flatMap to extract the street value const street = user.address.pipe( Option.flatMap((address) => address.street) ) console.log(street) // Output: { _id: 'Option', _tag: 'Some', value: '123 Main St' } ``` If `user.address` is `Some`, `Option.flatMap` applies the function `(address) => address.street` to retrieve the `street` value. If `user.address` is `None`, the function is not executed, and `street` remains `None`. This approach lets us handle nested optional values concisely, avoiding manual checks and making the code cleaner and easier to read. ### filter The `Option.filter` function allows you to filter an `Option` based on a given predicate. If the predicate is not met or if the `Option` is `None`, the result will be `None`. **Example** (Filtering an Option Value) Here's how you can simplify some code using `Option.filter` for a more idiomatic approach: Original Code ```ts twoslash import { Option } from "effect" // Function to remove empty strings from an Option const removeEmptyString = (input: Option.Option) => { if (Option.isSome(input) && input.value === "") { return Option.none() // Return None if the value is an empty string } return input // Otherwise, return the original Option } console.log(removeEmptyString(Option.none())) // Output: { _id: 'Option', _tag: 'None' } console.log(removeEmptyString(Option.some(""))) // Output: { _id: 'Option', _tag: 'None' } console.log(removeEmptyString(Option.some("a"))) // Output: { _id: 'Option', _tag: 'Some', value: 'a' } ``` Refactored Idiomatic Code Using `Option.filter`, we can write the same logic more concisely: ```ts twoslash import { Option } from "effect" const removeEmptyString = (input: Option.Option) => Option.filter(input, (value) => value !== "") console.log(removeEmptyString(Option.none())) // Output: { _id: 'Option', _tag: 'None' } console.log(removeEmptyString(Option.some(""))) // Output: { _id: 'Option', _tag: 'None' } console.log(removeEmptyString(Option.some("a"))) // Output: { _id: 'Option', _tag: 'Some', value: 'a' } ``` ## Getting the Value from an Option To retrieve the value stored inside an `Option`, you can use several helper functions provided by the `Option` module. Here's an overview of the available methods: ### getOrThrow This function extracts the value from a `Some`. If the `Option` is `None`, it throws an error. **Example** (Retrieving Value or Throwing an Error) ```ts twoslash import { Option } from "effect" console.log(Option.getOrThrow(Option.some(10))) // Output: 10 console.log(Option.getOrThrow(Option.none())) // throws: Error: getOrThrow called on a None ``` ### getOrNull / getOrUndefined These functions convert a `None` to either `null` or `undefined`, which is useful when working with non-`Option`-based code. **Example** (Converting `None` to `null` or `undefined`) ```ts twoslash import { Option } from "effect" console.log(Option.getOrNull(Option.some(5))) // Output: 5 console.log(Option.getOrNull(Option.none())) // Output: null console.log(Option.getOrUndefined(Option.some(5))) // Output: 5 console.log(Option.getOrUndefined(Option.none())) // Output: undefined ``` ### getOrElse This function allows you to specify a default value to return when the `Option` is `None`. **Example** (Providing a Default Value When `None`) ```ts twoslash import { Option } from "effect" console.log(Option.getOrElse(Option.some(5), () => 0)) // Output: 5 console.log(Option.getOrElse(Option.none(), () => 0)) // Output: 0 ``` ## Fallback ### orElse When a computation returns `None`, you might want to try an alternative computation that yields an `Option`. The `Option.orElse` function is helpful in such cases. It lets you chain multiple computations, moving on to the next if the current one results in `None`. This approach is often used in retry logic, attempting computations until one succeeds or all possibilities are exhausted. **Example** (Attempting Alternative Computations) ```ts twoslash import { Option } from "effect" // Simulating a computation that may or may not produce a result const computation = (): Option.Option => Math.random() < 0.5 ? Option.some(10) : Option.none() // Simulates an alternative computation const alternativeComputation = (): Option.Option => Math.random() < 0.5 ? Option.some(20) : Option.none() // Attempt the first computation, then try an alternative if needed const program = computation().pipe( Option.orElse(() => alternativeComputation()) ) const result = Option.match(program, { onNone: () => "Both computations resulted in None", // At least one computation succeeded onSome: (value) => `Computed value: ${value}` }) console.log(result) // Output: Computed value: 10 ``` ### firstSomeOf You can also use `Option.firstSomeOf` to get the first `Some` value from an iterable of `Option` values: **Example** (Retrieving the First `Some` Value) ```ts twoslash import { Option } from "effect" const first = Option.firstSomeOf([ Option.none(), Option.some(2), Option.none(), Option.some(3) ]) console.log(first) // Output: { _id: 'Option', _tag: 'Some', value: 2 } ``` ## Interop with Nullable Types When dealing with the `Option` data type, you may encounter code that uses `undefined` or `null` to represent optional values. The `Option` module provides several APIs to make interaction with these nullable types straightforward. ### fromNullable `Option.fromNullable` converts a nullable value (`null` or `undefined`) into an `Option`. If the value is `null` or `undefined`, it returns `Option.none()`. Otherwise, it wraps the value in `Option.some()`. **Example** (Creating Option from Nullable Values) ```ts twoslash import { Option } from "effect" console.log(Option.fromNullable(null)) // Output: { _id: 'Option', _tag: 'None' } console.log(Option.fromNullable(undefined)) // Output: { _id: 'Option', _tag: 'None' } console.log(Option.fromNullable(1)) // Output: { _id: 'Option', _tag: 'Some', value: 1 } ``` If you need to convert an `Option` back to a nullable value, there are two helper methods: - `Option.getOrNull`: Converts `None` to `null`. - `Option.getOrUndefined`: Converts `None` to `undefined`. ## Interop with Effect The `Option` type works as a subtype of the `Effect` type, allowing you to use it with functions from the `Effect` module. While these functions are built to handle `Effect` values, they can also manage `Option` values correctly. ### How Option Maps to Effect | Option Variant | Mapped to Effect | Description | | -------------- | --------------------------------------- | ---------------------------------- | | `None` | `Effect` | Represents the absence of a value | | `Some` | `Effect` | Represents the presence of a value | **Example** (Combining `Option` with `Effect`) ```ts twoslash import { Effect, Option } from "effect" // Function to get the head of an array, returning Option const head = (array: ReadonlyArray): Option.Option => array.length > 0 ? Option.some(array[0]) : Option.none() // Simulated fetch function that returns Effect const fetchData = (): Effect.Effect => { const success = Math.random() > 0.5 return success ? Effect.succeed("some data") : Effect.fail("Failed to fetch data") } // Mixing Either and Effect const program = Effect.all([head([1, 2, 3]), fetchData()]) Effect.runPromise(program).then(console.log) /* Example Output: [ 1, 'some data' ] */ ``` ## Combining Two or More Options ### zipWith The `Option.zipWith` function lets you combine two `Option` values using a provided function. It creates a new `Option` that holds the combined value of both original `Option` values. **Example** (Combining Two Options into an Object) ```ts twoslash import { Option } from "effect" const maybeName: Option.Option = Option.some("John") const maybeAge: Option.Option = Option.some(25) // Combine the name and age into a person object const person = Option.zipWith(maybeName, maybeAge, (name, age) => ({ name: name.toUpperCase(), age })) console.log(person) /* Output: { _id: 'Option', _tag: 'Some', value: { name: 'JOHN', age: 25 } } */ ``` If either of the `Option` values is `None`, the result will be `None`: **Example** (Handling None Values) ```ts {4} twoslash import { Option } from "effect" const maybeName: Option.Option = Option.some("John") const maybeAge: Option.Option = Option.none() // Since maybeAge is a None, the result will also be None const person = Option.zipWith(maybeName, maybeAge, (name, age) => ({ name: name.toUpperCase(), age })) console.log(person) // Output: { _id: 'Option', _tag: 'None' } ``` ### all To combine multiple `Option` values without transforming their contents, you can use `Option.all`. This function returns an `Option` with a structure matching the input: - If you pass a tuple, the result will be a tuple of the same length. - If you pass a struct, the result will be a struct with the same keys. - If you pass an `Iterable`, the result will be an array. **Example** (Combining Multiple Options into a Tuple and Struct) ```ts twoslash import { Option } from "effect" const maybeName: Option.Option = Option.some("John") const maybeAge: Option.Option = Option.some(25) // ┌─── Option<[string, number]> // ▼ const tuple = Option.all([maybeName, maybeAge]) console.log(tuple) /* Output: { _id: 'Option', _tag: 'Some', value: [ 'John', 25 ] } */ // ┌─── Option<{ name: string; age: number; }> // ▼ const struct = Option.all({ name: maybeName, age: maybeAge }) console.log(struct) /* Output: { _id: 'Option', _tag: 'Some', value: { name: 'John', age: 25 } } */ ``` If any of the `Option` values are `None`, the result will be `None`: **Example** ```ts import { Option } from "effect" const maybeName: Option.Option = Option.some("John") const maybeAge: Option.Option = Option.none() console.log(Option.all([maybeName, maybeAge])) // Output: { _id: 'Option', _tag: 'None' } ``` ## gen Similar to [Effect.gen](/docs/getting-started/using-generators/), `Option.gen` provides a more readable, generator-based syntax for working with `Option` values, making code that involves `Option` easier to write and understand. This approach is similar to using `async/await` but tailored for `Option`. **Example** (Using `Option.gen` to Create a Combined Value) ```ts twoslash import { Option } from "effect" const maybeName: Option.Option = Option.some("John") const maybeAge: Option.Option = Option.some(25) const person = Option.gen(function* () { const name = (yield* maybeName).toUpperCase() const age = yield* maybeAge return { name, age } }) console.log(person) /* Output: { _id: 'Option', _tag: 'Some', value: { name: 'JOHN', age: 25 } } */ ``` When any of the `Option` values in the sequence is `None`, the generator immediately returns the `None` value, skipping further operations: **Example** (Handling a `None` Value with `Option.gen`) In this example, `Option.gen` halts execution as soon as it encounters the `None` value, effectively propagating the missing value without performing further operations. ```ts twoslash import { Option } from "effect" const maybeName: Option.Option = Option.none() const maybeAge: Option.Option = Option.some(25) const program = Option.gen(function* () { console.log("Retrieving name...") const name = (yield* maybeName).toUpperCase() console.log("Retrieving age...") const age = yield* maybeAge return { name, age } }) console.log(program) /* Output: Retrieving name... { _id: 'Option', _tag: 'None' } */ ``` The use of `console.log` in these example is for demonstration purposes only. When using `Option.gen`, avoid including side effects in your generator functions, as `Option` should remain a pure data structure. ## Equivalence You can compare `Option` values using the `Option.getEquivalence` function. This function allows you to specify how to compare the contents of `Option` types by providing an [Equivalence](/docs/behaviour/equivalence/) for the type of value they may contain. **Example** (Comparing Optional Numbers for Equivalence) Suppose you have optional numbers and want to check if they are equivalent. Here's how you can use `Option.getEquivalence`: ```ts twoslash import { Option, Equivalence } from "effect" const myEquivalence = Option.getEquivalence(Equivalence.number) console.log(myEquivalence(Option.some(1), Option.some(1))) // Output: true, both options contain the number 1 console.log(myEquivalence(Option.some(1), Option.some(2))) // Output: false, the numbers are different console.log(myEquivalence(Option.some(1), Option.none())) // Output: false, one is a number and the other is empty ``` ## Sorting You can sort a collection of `Option` values using the `Option.getOrder` function. This function helps specify a custom sorting rule for the type of value contained within the `Option`. **Example** (Sorting Optional Numbers) Suppose you have a list of optional numbers and want to sort them in ascending order, with empty values (`Option.none()`) treated as the lowest: ```ts twoslash import { Option, Array, Order } from "effect" const items = [Option.some(1), Option.none(), Option.some(2)] // Create an order for sorting Option values containing numbers const myOrder = Option.getOrder(Order.number) console.log(Array.sort(myOrder)(items)) /* Output: [ { _id: 'Option', _tag: 'None' }, // None appears first because it's considered the lowest { _id: 'Option', _tag: 'Some', value: 1 }, // Sorted in ascending order { _id: 'Option', _tag: 'Some', value: 2 } ] */ ``` **Example** (Sorting Optional Dates in Reverse Order) Consider a more complex case where you have a list of objects containing optional dates, and you want to sort them in descending order, with `Option.none()` values at the end: ```ts import { Option, Array, Order } from "effect" const items = [ { data: Option.some(new Date(10)) }, { data: Option.some(new Date(20)) }, { data: Option.none() } ] // Define the order to sort dates within Option values in reverse const sorted = Array.sortWith( items, (item) => item.data, Order.reverse(Option.getOrder(Order.Date)) ) console.log(sorted) /* Output: [ { data: { _id: 'Option', _tag: 'Some', value: '1970-01-01T00:00:00.020Z' } }, { data: { _id: 'Option', _tag: 'Some', value: '1970-01-01T00:00:00.010Z' } }, { data: { _id: 'Option', _tag: 'None' } } // None placed last ] */ ``` # [Redacted](https://effect.website/docs/data-types/redacted/) ## Overview The Redacted module provides functionality for handling sensitive information securely within your application. By using the `Redacted` data type, you can ensure that sensitive values are not accidentally exposed in logs or error messages. ## make The `Redacted.make` function creates a `Redacted` instance from a given value `A`, ensuring the content is securely hidden. **Example** (Hiding Sensitive Information from Logs) Using `Redacted.make` helps prevent sensitive information, such as API keys, from being accidentally exposed in logs or error messages. ```ts twoslash import { Redacted, Effect } from "effect" // Create a redacted API key const API_KEY = Redacted.make("1234567890") console.log(API_KEY) // Output: {} console.log(String(API_KEY)) // Output: Effect.runSync(Effect.log(API_KEY)) // Output: timestamp=... level=INFO fiber=#0 message="\"\"" ``` ## value The `Redacted.value` function retrieves the original value from a `Redacted` instance. Use this function carefully, as it exposes the sensitive data, potentially making it visible in logs or accessible in unintended ways. **Example** (Accessing the Underlying Sensitive Value) ```ts twoslash import { Redacted } from "effect" const API_KEY = Redacted.make("1234567890") // Expose the redacted value console.log(Redacted.value(API_KEY)) // Output: "1234567890" ``` ## unsafeWipe The `Redacted.unsafeWipe` function erases the underlying value of a `Redacted` instance, making it inaccessible. This helps ensure that sensitive data does not remain in memory longer than needed. **Example** (Wiping Sensitive Data from Memory) ```ts twoslash import { Redacted } from "effect" const API_KEY = Redacted.make("1234567890") console.log(Redacted.value(API_KEY)) // Output: "1234567890" Redacted.unsafeWipe(API_KEY) console.log(Redacted.value(API_KEY)) /* throws: Error: Unable to get redacted value */ ``` ## getEquivalence The `Redacted.getEquivalence` function generates an [Equivalence](/docs/behaviour/equivalence/) for `Redacted` values using an Equivalence for the underlying values of type `A`. This allows you to compare `Redacted` values securely without revealing their content. **Example** (Comparing Redacted Values) ```ts twoslash import { Redacted, Equivalence } from "effect" const API_KEY1 = Redacted.make("1234567890") const API_KEY2 = Redacted.make("1-34567890") const API_KEY3 = Redacted.make("1234567890") const equivalence = Redacted.getEquivalence(Equivalence.string) console.log(equivalence(API_KEY1, API_KEY2)) // Output: false console.log(equivalence(API_KEY1, API_KEY3)) // Output: true ``` # [Error Accumulation](https://effect.website/docs/error-management/error-accumulation/) ## Overview import { Aside } from "@astrojs/starlight/components" Sequential combinators such as [Effect.zip](/docs/getting-started/control-flow/#zip), [Effect.all](/docs/getting-started/control-flow/#all) and [Effect.forEach](/docs/getting-started/control-flow/#foreach) have a "fail fast" policy when it comes to error management. This means that they stop and return immediately when they encounter the first error. Here's an example using `Effect.zip`, which stops at the first failure and only shows the first error: **Example** (Fail Fast with `Effect.zip`) ```ts twoslash import { Effect, Console } from "effect" const task1 = Console.log("task1").pipe(Effect.as(1)) const task2 = Effect.fail("Oh uh!").pipe(Effect.as(2)) const task3 = Console.log("task2").pipe(Effect.as(3)) const task4 = Effect.fail("Oh no!").pipe(Effect.as(4)) const program = task1.pipe( Effect.zip(task2), Effect.zip(task3), Effect.zip(task4) ) Effect.runPromise(program).then(console.log, console.error) /* Output: task1 (FiberFailure) Error: Oh uh! */ ``` The `Effect.forEach` function behaves similarly. It applies an effectful operation to each element in a collection, but will stop when it hits the first error: **Example** (Fail Fast with `Effect.forEach`) ```ts twoslash import { Effect, Console } from "effect" const program = Effect.forEach([1, 2, 3, 4, 5], (n) => { if (n < 4) { return Console.log(`item ${n}`).pipe(Effect.as(n)) } else { return Effect.fail(`${n} is not less that 4`) } }) Effect.runPromise(program).then(console.log, console.error) /* Output: item 1 item 2 item 3 (FiberFailure) Error: 4 is not less that 4 */ ``` However, there are cases where you may want to collect all errors rather than fail fast. In these situations, you can use functions that accumulate both successes and errors. ## validate The `Effect.validate` function is similar to `Effect.zip`, but it continues combining effects even after encountering errors, accumulating both successes and failures. **Example** (Validating and Collecting Errors) ```ts twoslash import { Effect, Console } from "effect" const task1 = Console.log("task1").pipe(Effect.as(1)) const task2 = Effect.fail("Oh uh!").pipe(Effect.as(2)) const task3 = Console.log("task2").pipe(Effect.as(3)) const task4 = Effect.fail("Oh no!").pipe(Effect.as(4)) const program = task1.pipe( Effect.validate(task2), Effect.validate(task3), Effect.validate(task4) ) Effect.runPromiseExit(program).then(console.log) /* Output: task1 task2 { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Sequential', left: { _id: 'Cause', _tag: 'Fail', failure: 'Oh uh!' }, right: { _id: 'Cause', _tag: 'Fail', failure: 'Oh no!' } } } */ ``` ## validateAll The `Effect.validateAll` function is similar to the `Effect.forEach` function. It transforms all elements of a collection using the provided effectful operation, but it collects all errors in the error channel, as well as the success values in the success channel. ```ts twoslash import { Effect, Console } from "effect" // ┌─── Effect // ▼ const program = Effect.validateAll([1, 2, 3, 4, 5], (n) => { if (n < 4) { return Console.log(`item ${n}`).pipe(Effect.as(n)) } else { return Effect.fail(`${n} is not less that 4`) } }) Effect.runPromiseExit(program).then(console.log) /* Output: item 1 item 2 item 3 { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: [ '4 is not less that 4', '5 is not less that 4' ] } } */ ``` ## validateFirst The `Effect.validateFirst` function is similar to `Effect.validateAll` but it returns the first successful result, or all errors if none succeed. **Example** (Returning the First Success) ```ts twoslash import { Effect, Console } from "effect" // ┌─── Effect // ▼ const program = Effect.validateFirst([1, 2, 3, 4, 5], (n) => { if (n < 4) { return Effect.fail(`${n} is not less that 4`) } else { return Console.log(`item ${n}`).pipe(Effect.as(n)) } }) Effect.runPromise(program).then(console.log, console.error) /* Output: item 4 4 */ ``` Notice that `Effect.validateFirst` returns a single `number` as the success type, rather than an array of results like `Effect.validateAll`. ## partition The `Effect.partition` function processes an iterable and applies an effectful function to each element. It returns a tuple, where the first part contains all the failures, and the second part contains all the successes. **Example** (Partitioning Successes and Failures) ```ts twoslash import { Effect } from "effect" // ┌─── Effect<[string[], number[]], never, never> // ▼ const program = Effect.partition([0, 1, 2, 3, 4], (n) => { if (n % 2 === 0) { return Effect.succeed(n) } else { return Effect.fail(`${n} is not even`) } }) Effect.runPromise(program).then(console.log, console.error) /* Output: [ [ '1 is not even', '3 is not even' ], [ 0, 2, 4 ] ] */ ``` This operator is an unexceptional effect, meaning the error channel type is `never`. Failures are collected without stopping the effect, so the entire operation completes and returns both errors and successes. # [Error Channel Operations](https://effect.website/docs/error-management/error-channel-operations/) ## Overview import { Aside } from "@astrojs/starlight/components" In Effect you can perform various operations on the error channel of effects. These operations allow you to transform, inspect, and handle errors in different ways. Let's explore some of these operations. ## Map Operations ### mapError The `Effect.mapError` function is used when you need to transform or modify an error produced by an effect, without affecting the success value. This can be helpful when you want to add extra information to the error or change its type. **Example** (Mapping an Error) Here, the error type changes from `string` to `Error`. ```ts twoslash import { Effect } from "effect" // ┌─── Effect // ▼ const simulatedTask = Effect.fail("Oh no!").pipe(Effect.as(1)) // ┌─── Effect // ▼ const mapped = Effect.mapError( simulatedTask, (message) => new Error(message) ) ``` ### mapBoth The `Effect.mapBoth` function allows you to apply transformations to both channels: the error channel and the success channel of an effect. It takes two map functions as arguments: one for the error channel and the other for the success channel. **Example** (Mapping Both Success and Error) ```ts twoslash import { Effect } from "effect" // ┌─── Effect // ▼ const simulatedTask = Effect.fail("Oh no!").pipe(Effect.as(1)) // ┌─── Effect // ▼ const modified = Effect.mapBoth(simulatedTask, { onFailure: (message) => new Error(message), onSuccess: (n) => n > 0 }) ``` ## Filtering the Success Channel The Effect library provides several operators to filter values on the success channel based on a given predicate. These operators offer different strategies for handling cases where the predicate fails: | API | Description | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `filterOrFail` | This operator filters the values on the success channel based on a predicate. If the predicate fails for any value, the original effect fails with an error. | | `filterOrDie` / `filterOrDieMessage` | These operators also filter the values on the success channel based on a predicate. If the predicate fails for any value, the original effect terminates abruptly. The `filterOrDieMessage` variant allows you to provide a custom error message. | | `filterOrElse` | This operator filters the values on the success channel based on a predicate. If the predicate fails for any value, an alternative effect is executed instead. | **Example** (Filtering Success Values) ```ts twoslash import { Effect, Random, Cause } from "effect" // Fail with a custom error if predicate is false const task1 = Effect.filterOrFail( Random.nextRange(-1, 1), (n) => n >= 0, () => "random number is negative" ) // Die with a custom exception if predicate is false const task2 = Effect.filterOrDie( Random.nextRange(-1, 1), (n) => n >= 0, () => new Cause.IllegalArgumentException("random number is negative") ) // Die with a custom error message if predicate is false const task3 = Effect.filterOrDieMessage( Random.nextRange(-1, 1), (n) => n >= 0, "random number is negative" ) // Run an alternative effect if predicate is false const task4 = Effect.filterOrElse( Random.nextRange(-1, 1), (n) => n >= 0, () => task3 ) ``` It's important to note that depending on the specific filtering operator used, the effect can either fail, terminate abruptly, or execute an alternative effect when the predicate fails. Choose the appropriate operator based on your desired error handling strategy and program logic. The filtering APIs can also be combined with [user-defined type guards](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) to improve type safety and code clarity. This ensures that only valid types pass through. **Example** (Using a Type Guard) ```ts twoslash {15} import { Effect, pipe } from "effect" // Define a user interface interface User { readonly name: string } // Simulate an asynchronous authentication function declare const auth: () => Promise const program = pipe( Effect.promise(() => auth()), // Use filterOrFail with a custom type guard to ensure user is not null Effect.filterOrFail( (user): user is User => user !== null, // Type guard () => new Error("Unauthorized") ), // 'user' now has the type `User` (not `User | null`) Effect.andThen((user) => user.name) ) ``` In the example above, a guard is used within the `filterOrFail` API to ensure that the `user` is of type `User` rather than `User | null`. If you prefer, you can utilize a pre-made guard like [Predicate.isNotNull](https://effect-ts.github.io/effect/effect/Predicate.ts.html#isnotnull) for simplicity and consistency. ## Inspecting Errors Similar to [tapping](/docs/getting-started/building-pipelines/#tap) for success values, Effect provides several operators for inspecting error values. These operators allow developers to observe failures or underlying issues without modifying the outcome. ### tapError Executes an effectful operation to inspect the failure of an effect without altering it. **Example** (Inspecting Errors) ```ts twoslash import { Effect, Console } from "effect" // Simulate a task that fails with an error const task: Effect.Effect = Effect.fail("NetworkError") // Use tapError to log the error message when the task fails const tapping = Effect.tapError(task, (error) => Console.log(`expected error: ${error}`) ) Effect.runFork(tapping) /* Output: expected error: NetworkError */ ``` ### tapErrorTag This function allows you to inspect errors that match a specific tag, helping you handle different error types more precisely. **Example** (Inspecting Tagged Errors) ```ts twoslash import { Effect, Console } from "effect" class NetworkError { readonly _tag = "NetworkError" constructor(readonly statusCode: number) {} } class ValidationError { readonly _tag = "ValidationError" constructor(readonly field: string) {} } // Create a task that fails with a NetworkError const task: Effect.Effect = Effect.fail(new NetworkError(504)) // Use tapErrorTag to inspect only NetworkError types and log the status code const tapping = Effect.tapErrorTag(task, "NetworkError", (error) => Console.log(`expected error: ${error.statusCode}`) ) Effect.runFork(tapping) /* Output: expected error: 504 */ ``` ### tapErrorCause This function inspects the complete cause of an error, including failures and defects. **Example** (Inspecting Error Causes) ```ts twoslash import { Effect, Console } from "effect" // Create a task that fails with a NetworkError const task1: Effect.Effect = Effect.fail("NetworkError") const tapping1 = Effect.tapErrorCause(task1, (cause) => Console.log(`error cause: ${cause}`) ) Effect.runFork(tapping1) /* Output: error cause: Error: NetworkError */ // Simulate a severe failure in the system const task2: Effect.Effect = Effect.dieMessage( "Something went wrong" ) const tapping2 = Effect.tapErrorCause(task2, (cause) => Console.log(`error cause: ${cause}`) ) Effect.runFork(tapping2) /* Output: error cause: RuntimeException: Something went wrong ... stack trace ... */ ``` ### tapDefect Specifically inspects non-recoverable failures or defects in an effect (i.e., one or more [Die](/docs/data-types/cause/#die) causes). **Example** (Inspecting Defects) ```ts twoslash import { Effect, Console } from "effect" // Simulate a task that fails with a recoverable error const task1: Effect.Effect = Effect.fail("NetworkError") // tapDefect won't log anything because NetworkError is not a defect const tapping1 = Effect.tapDefect(task1, (cause) => Console.log(`defect: ${cause}`) ) Effect.runFork(tapping1) /* No Output */ // Simulate a severe failure in the system const task2: Effect.Effect = Effect.dieMessage( "Something went wrong" ) // Log the defect using tapDefect const tapping2 = Effect.tapDefect(task2, (cause) => Console.log(`defect: ${cause}`) ) Effect.runFork(tapping2) /* Output: defect: RuntimeException: Something went wrong ... stack trace ... */ ``` ### tapBoth Inspects both success and failure outcomes of an effect, performing different actions based on the result. **Example** (Inspecting Both Success and Failure) ```ts twoslash import { Effect, Random, Console } from "effect" // Simulate a task that might fail const task = Effect.filterOrFail( Random.nextRange(-1, 1), (n) => n >= 0, () => "random number is negative" ) // Use tapBoth to log both success and failure outcomes const tapping = Effect.tapBoth(task, { onFailure: (error) => Console.log(`failure: ${error}`), onSuccess: (randomNumber) => Console.log(`random number: ${randomNumber}`) }) Effect.runFork(tapping) /* Example Output: failure: random number is negative */ ``` ## Exposing Errors in The Success Channel The `Effect.either` function transforms an `Effect` into an effect that encapsulates both potential failure and success within an [Either](/docs/data-types/either/) data type: ```ts showLineNumbers=false Effect -> Effect, never, R> ``` This means if you have an effect with the following type: ```ts showLineNumbers=false Effect ``` and you call `Effect.either` on it, the type becomes: ```ts showLineNumbers=false Effect, never, never> ``` The resulting effect cannot fail because the potential failure is now represented within the `Either`'s `Left` type. The error type of the returned `Effect` is specified as `never`, confirming that the effect is structured to not fail. This function becomes especially useful when recovering from effects that may fail when using [Effect.gen](/docs/getting-started/using-generators/#understanding-effectgen): **Example** (Using `Effect.either` to Handle Errors) ```ts twoslash import { Effect, Either, Console } from "effect" // Simulate a task that fails // // ┌─── Either // ▼ const program = Effect.fail("Oh uh!").pipe(Effect.as(2)) // ┌─── Either // ▼ const recovered = Effect.gen(function* () { // ┌─── Either // ▼ const failureOrSuccess = yield* Effect.either(program) if (Either.isLeft(failureOrSuccess)) { const error = failureOrSuccess.left yield* Console.log(`failure: ${error}`) return 0 } else { const value = failureOrSuccess.right yield* Console.log(`success: ${value}`) return value } }) Effect.runPromise(recovered).then(console.log) /* Output: failure: Oh uh! 0 */ ``` ## Exposing the Cause in The Success Channel You can use the `Effect.cause` function to expose the cause of an effect, which is a more detailed representation of failures, including error messages and defects. **Example** (Logging the Cause of Failure) ```ts twoslash import { Effect, Console } from "effect" // ┌─── Effect // ▼ const program = Effect.fail("Oh uh!").pipe(Effect.as(2)) // ┌─── Effect // ▼ const recovered = Effect.gen(function* () { const cause = yield* Effect.cause(program) yield* Console.log(cause) }) ``` ## Merging the Error Channel into the Success Channel The `Effect.merge` function allows you to combine the error channel with the success channel. This results in an effect that never fails; instead, both successes and errors are handled as values in the success channel. **Example** (Combining Error and Success Channels) ```ts twoslash import { Effect } from "effect" // ┌─── Effect // ▼ const program = Effect.fail("Oh uh!").pipe(Effect.as(2)) // ┌─── Effect // ▼ const recovered = Effect.merge(program) ``` ## Flipping Error and Success Channels The `Effect.flip` function allows you to switch the error and success channels of an effect. This means that what was previously a success becomes the error, and vice versa. **Example** (Swapping Error and Success Channels) ```ts twoslash import { Effect } from "effect" // ┌─── Effect // ▼ const program = Effect.fail("Oh uh!").pipe(Effect.as(2)) // ┌─── Effect // ▼ const flipped = Effect.flip(program) ``` # [Expected Errors](https://effect.website/docs/error-management/expected-errors/) ## Overview import { Aside } from "@astrojs/starlight/components" Expected errors are tracked at the type level by the [Effect data type](/docs/getting-started/the-effect-type/) in the "Error channel": ```text showLineNumbers=false "Error" ┌─── Represents the success type │ ┌─── Represents the error type │ │ ┌─── Represents required dependencies ▼ ▼ ▼ Effect ``` This means that the `Effect` type captures not only what the program returns on success but also what type of error it might produce. **Example** (Creating an Effect That Can Fail) In this example, we define a program that might randomly fail with an `HttpError`. ```ts twoslash import { Effect, Random } from "effect" class HttpError { readonly _tag = "HttpError" } // ┌─── Effect // ▼ const program = Effect.gen(function* () { // Generate a random number between 0 and 1 const n = yield* Random.next // Simulate an HTTP error if (n < 0.5) { yield* Effect.fail(new HttpError()) } return "some result" }) ``` The type of `program` tells us that it can either return a `string` or fail with an `HttpError`: ```ts "string" "HttpError" showLineNumbers=false const program: Effect ``` In this case, we use a class to represent the `HttpError` type, which allows us to define both the error type and a constructor. However, you can use whatever you like to model your error types. It's also worth noting that we added a readonly `_tag` field to the class: ```ts showLineNumbers=false class HttpError { // This field serves as a discriminant for the error readonly _tag = "HttpError" } ``` This discriminant field will be useful when we discuss APIs like [Effect.catchTag](#catchtag), which help in handling specific error types. ## Error Tracking In Effect, if a program can fail with multiple types of errors, they are automatically tracked as a union of those error types. This allows you to know exactly what errors can occur during execution, making error handling more precise and predictable. The example below illustrates how errors are automatically tracked for you. **Example** (Automatically Tracking Errors) ```ts twoslash import { Effect, Random } from "effect" class HttpError { readonly _tag = "HttpError" } class ValidationError { readonly _tag = "ValidationError" } // ┌─── Effect // ▼ const program = Effect.gen(function* () { // Generate two random numbers between 0 and 1 const n1 = yield* Random.next const n2 = yield* Random.next // Simulate an HTTP error if (n1 < 0.5) { yield* Effect.fail(new HttpError()) } // Simulate a validation error if (n2 < 0.5) { yield* Effect.fail(new ValidationError()) } return "some result" }) ``` Effect automatically keeps track of the possible errors that can occur during the execution of the program as a union: ```ts "HttpError | ValidationError" showLineNumbers=false const program: Effect ``` indicating that it can potentially fail with either a `HttpError` or a `ValidationError`. ## Short-Circuiting When working with APIs like [Effect.gen](/docs/getting-started/using-generators/#understanding-effectgen), [Effect.map](/docs/getting-started/building-pipelines/#map), [Effect.flatMap](/docs/getting-started/building-pipelines/#flatmap), and [Effect.andThen](/docs/getting-started/building-pipelines/#andthen), it's important to understand how they handle errors. These APIs are designed to **short-circuit the execution** upon encountering the **first error**. What does this mean for you as a developer? Well, let's say you have a chain of operations or a collection of effects to be executed in sequence. If any error occurs during the execution of one of these effects, the remaining computations will be skipped, and the error will be propagated to the final result. In simpler terms, the short-circuiting behavior ensures that if something goes wrong at any step of your program, it won't waste time executing unnecessary computations. Instead, it will immediately stop and return the error to let you know that something went wrong. **Example** (Short-Circuiting Behavior) ```ts twoslash {14-15} import { Effect, Console } from "effect" // Define three effects representing different tasks. const task1 = Console.log("Executing task1...") const task2 = Effect.fail("Something went wrong!") const task3 = Console.log("Executing task3...") // Compose the three tasks to run them in sequence. // If one of the tasks fails, the subsequent tasks won't be executed. const program = Effect.gen(function* () { yield* task1 // After task1, task2 is executed, but it fails with an error yield* task2 // This computation won't be executed because the previous one fails yield* task3 }) Effect.runPromiseExit(program).then(console.log) /* Output: Executing task1... { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Something went wrong!' } } */ ``` This code snippet demonstrates the short-circuiting behavior when an error occurs. Each operation depends on the successful execution of the previous one. If any error occurs, the execution is short-circuited, and the error is propagated. In this specific example, `task3` is never executed because an error occurs in `task2`. ## Catching All Errors ### either The `Effect.either` function transforms an `Effect` into an effect that encapsulates both potential failure and success within an [Either](/docs/data-types/either/) data type: ```ts showLineNumbers=false Effect -> Effect, never, R> ``` This means if you have an effect with the following type: ```ts showLineNumbers=false Effect ``` and you call `Effect.either` on it, the type becomes: ```ts showLineNumbers=false Effect, never, never> ``` The resulting effect cannot fail because the potential failure is now represented within the `Either`'s `Left` type. The error type of the returned `Effect` is specified as `never`, confirming that the effect is structured to not fail. By yielding an `Either`, we gain the ability to "pattern match" on this type to handle both failure and success cases within the generator function. **Example** (Using `Effect.either` to Handle Errors) ```ts twoslash import { Effect, Either, Random } from "effect" class HttpError { readonly _tag = "HttpError" } class ValidationError { readonly _tag = "ValidationError" } // ┌─── Effect // ▼ const program = Effect.gen(function* () { const n1 = yield* Random.next const n2 = yield* Random.next if (n1 < 0.5) { yield* Effect.fail(new HttpError()) } if (n2 < 0.5) { yield* Effect.fail(new ValidationError()) } return "some result" }) // ┌─── Effect // ▼ const recovered = Effect.gen(function* () { // ┌─── Either // ▼ const failureOrSuccess = yield* Effect.either(program) if (Either.isLeft(failureOrSuccess)) { // Failure case: you can extract the error from the `left` property const error = failureOrSuccess.left return `Recovering from ${error._tag}` } else { // Success case: you can extract the value from the `right` property return failureOrSuccess.right } }) ``` As you can see since all errors are handled, the error type of the resulting effect `recovered` is `never`: ```ts showLineNumbers=false const recovered: Effect ``` We can make the code less verbose by using the `Either.match` function, which directly accepts the two callback functions for handling errors and successful values: **Example** (Simplifying with `Either.match`) ```ts twoslash collapse={3-23} import { Effect, Either, Random } from "effect" class HttpError { readonly _tag = "HttpError" } class ValidationError { readonly _tag = "ValidationError" } // ┌─── Effect // ▼ const program = Effect.gen(function* () { const n1 = yield* Random.next const n2 = yield* Random.next if (n1 < 0.5) { yield* Effect.fail(new HttpError()) } if (n2 < 0.5) { yield* Effect.fail(new ValidationError()) } return "some result" }) // ┌─── Effect // ▼ const recovered = Effect.gen(function* () { // ┌─── Either // ▼ const failureOrSuccess = yield* Effect.either(program) return Either.match(failureOrSuccess, { onLeft: (error) => `Recovering from ${error._tag}`, onRight: (value) => value // Do nothing in case of success }) }) ``` ### option Transforms an effect to encapsulate both failure and success using the [Option](/docs/data-types/option/) data type. The `Effect.option` function wraps the success or failure of an effect within the `Option` type, making both cases explicit. If the original effect succeeds, its value is wrapped in `Option.some`. If it fails, the failure is mapped to `Option.none`. The resulting effect cannot fail directly, as the error type is set to `never`. However, fatal errors like defects are not encapsulated. **Example** (Using `Effect.option` to Handle Errors) ```ts twoslash import { Effect } from "effect" const maybe1 = Effect.option(Effect.succeed(1)) Effect.runPromiseExit(maybe1).then(console.log) /* Output: { _id: 'Exit', _tag: 'Success', value: { _id: 'Option', _tag: 'Some', value: 1 } } */ const maybe2 = Effect.option(Effect.fail("Uh oh!")) Effect.runPromiseExit(maybe2).then(console.log) /* Output: { _id: 'Exit', _tag: 'Success', value: { _id: 'Option', _tag: 'None' } } */ const maybe3 = Effect.option(Effect.die("Boom!")) Effect.runPromiseExit(maybe3).then(console.log) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Die', defect: 'Boom!' } } */ ``` ### catchAll Handles all errors in an effect by providing a fallback effect. The `Effect.catchAll` function catches any errors that may occur during the execution of an effect and allows you to handle them by specifying a fallback effect. This ensures that the program continues without failing by recovering from errors using the provided fallback logic. **Example** (Providing Recovery Logic for Recoverable Errors) ```ts twoslash import { Effect, Random } from "effect" class HttpError { readonly _tag = "HttpError" } class ValidationError { readonly _tag = "ValidationError" } // ┌─── Effect // ▼ const program = Effect.gen(function* () { const n1 = yield* Random.next const n2 = yield* Random.next if (n1 < 0.5) { yield* Effect.fail(new HttpError()) } if (n2 < 0.5) { yield* Effect.fail(new ValidationError()) } return "some result" }) // ┌─── Effect // ▼ const recovered = program.pipe( Effect.catchAll((error) => Effect.succeed(`Recovering from ${error._tag}`) ) ) ``` We can observe that the type in the error channel of our program has changed to `never`: ```ts showLineNumbers=false const recovered: Effect ``` indicating that all errors have been handled. ### catchAllCause Handles both recoverable and unrecoverable errors by providing a recovery effect. The `Effect.catchAllCause` function allows you to handle all errors, including unrecoverable defects, by providing a recovery effect. The recovery logic is based on the `Cause` of the error, which provides detailed information about the failure. **Example** (Recovering from All Errors) ```ts twoslash import { Cause, Effect } from "effect" // Define an effect that may fail with a recoverable or unrecoverable error const program = Effect.fail("Something went wrong!") // Recover from all errors by examining the cause const recovered = program.pipe( Effect.catchAllCause((cause) => Cause.isFailType(cause) ? Effect.succeed("Recovered from a regular error") : Effect.succeed("Recovered from a defect") ) ) Effect.runPromise(recovered).then(console.log) // Output: "Recovered from a regular error" ``` ## Catching Some Errors ### either The [`Effect.either`](#either) function, which was previously shown as a way to catch all errors, can also be used to catch specific errors. By yielding an `Either`, we gain the ability to "pattern match" on this type to handle both failure and success cases within the generator function. **Example** (Handling Specific Errors with `Effect.either`) ```ts twoslash import { Effect, Random, Either } from "effect" class HttpError { readonly _tag = "HttpError" } class ValidationError { readonly _tag = "ValidationError" } // ┌─── Effect // ▼ const program = Effect.gen(function* () { const n1 = yield* Random.next const n2 = yield* Random.next if (n1 < 0.5) { yield* Effect.fail(new HttpError()) } if (n2 < 0.5) { yield* Effect.fail(new ValidationError()) } return "some result" }) // ┌─── Effect // ▼ const recovered = Effect.gen(function* () { const failureOrSuccess = yield* Effect.either(program) if (Either.isLeft(failureOrSuccess)) { const error = failureOrSuccess.left // Only handle HttpError errors if (error._tag === "HttpError") { return "Recovering from HttpError" } else { // Rethrow ValidationError return yield* Effect.fail(error) } } else { return failureOrSuccess.right } }) ``` We can observe that the type in the error channel of our program has changed to only show `ValidationError`: ```ts "ValidationError" showLineNumbers=false const recovered: Effect ``` indicating that `HttpError` has been handled. If we also want to handle `ValidationError`, we can easily add another case to our code: ```ts twoslash collapse={3-21} {32-34} import { Effect, Random, Either } from "effect" class HttpError { readonly _tag = "HttpError" } class ValidationError { readonly _tag = "ValidationError" } const program = Effect.gen(function* () { const n1 = yield* Random.next const n2 = yield* Random.next if (n1 < 0.5) { yield* Effect.fail(new HttpError()) } if (n2 < 0.5) { yield* Effect.fail(new ValidationError()) } return "some result" }) // ┌─── Effect // ▼ const recovered = Effect.gen(function* () { const failureOrSuccess = yield* Effect.either(program) if (Either.isLeft(failureOrSuccess)) { const error = failureOrSuccess.left // Handle both HttpError and ValidationError if (error._tag === "HttpError") { return "Recovering from HttpError" } else { return "Recovering from ValidationError" } } else { return failureOrSuccess.right } }) ``` We can observe that the type in the error channel has changed to `never`: ```ts showLineNumbers=false const recovered: Effect ``` indicating that all errors have been handled. ### catchSome Catches and recovers from specific types of errors, allowing you to attempt recovery only for certain errors. `Effect.catchSome` lets you selectively catch and handle errors of certain types by providing a recovery effect for specific errors. If the error matches a condition, recovery is attempted; if not, it doesn't affect the program. This function doesn't alter the error type, meaning the error type remains the same as in the original effect. **Example** (Handling Specific Errors with `Effect.catchSome`) ```ts twoslash import { Effect, Random, Option } from "effect" class HttpError { readonly _tag = "HttpError" } class ValidationError { readonly _tag = "ValidationError" } // ┌─── Effect // ▼ const program = Effect.gen(function* () { const n1 = yield* Random.next const n2 = yield* Random.next if (n1 < 0.5) { yield* Effect.fail(new HttpError()) } if (n2 < 0.5) { yield* Effect.fail(new ValidationError()) } return "some result" }) // ┌─── Effect // ▼ const recovered = program.pipe( Effect.catchSome((error) => { // Only handle HttpError errors if (error._tag === "HttpError") { return Option.some(Effect.succeed("Recovering from HttpError")) } else { return Option.none() } }) ) ``` In the code above, `Effect.catchSome` takes a function that examines the error and decides whether to attempt recovery or not. If the error matches a specific condition, recovery can be attempted by returning `Option.some(effect)`. If no recovery is possible, you can simply return `Option.none()`. It's important to note that while `Effect.catchSome` lets you catch specific errors, it doesn't alter the error type itself. Therefore, the resulting effect will still have the same error type as the original effect: ```ts "HttpError | ValidationError" showLineNumbers=false const recovered: Effect ``` ### catchIf Recovers from specific errors based on a predicate. `Effect.catchIf` works similarly to [`Effect.catchSome`](#catchsome), but it allows you to recover from errors by providing a predicate function. If the predicate matches the error, the recovery effect is applied. This function doesn't alter the error type, so the resulting effect still carries the original error type unless a user-defined type guard is used to narrow the type. **Example** (Catching Specific Errors with a Predicate) ```ts twoslash collapse={3-21 import { Effect, Random } from "effect" class HttpError { readonly _tag = "HttpError" } class ValidationError { readonly _tag = "ValidationError" } // ┌─── Effect // ▼ const program = Effect.gen(function* () { const n1 = yield* Random.next const n2 = yield* Random.next if (n1 < 0.5) { yield* Effect.fail(new HttpError()) } if (n2 < 0.5) { yield* Effect.fail(new ValidationError()) } return "some result" }) // ┌─── Effect // ▼ const recovered = program.pipe( Effect.catchIf( // Only handle HttpError errors (error) => error._tag === "HttpError", () => Effect.succeed("Recovering from HttpError") ) ) ``` It's important to note that for TypeScript versions < 5.5, while `Effect.catchIf` lets you catch specific errors, it **doesn't alter the error type** itself. Therefore, the resulting effect will still have the same error type as the original effect: ```ts "HttpError | ValidationError" showLineNumbers=false const recovered: Effect ``` In TypeScript versions >= 5.5, improved type narrowing causes the resulting error type to be inferred as `ValidationError`. #### Workaround For TypeScript versions < 5.5 If you provide a [user-defined type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) instead of a predicate, the resulting error type will be pruned, returning an `Effect`: ```ts twoslash collapse={3-23} {29-30} import { Effect, Random } from "effect" class HttpError { readonly _tag = "HttpError" } class ValidationError { readonly _tag = "ValidationError" } // ┌─── Effect // ▼ const program = Effect.gen(function* () { const n1 = yield* Random.next const n2 = yield* Random.next if (n1 < 0.5) { yield* Effect.fail(new HttpError()) } if (n2 < 0.5) { yield* Effect.fail(new ValidationError()) } return "some result" }) // ┌─── Effect // ▼ const recovered = program.pipe( Effect.catchIf( // User-defined type guard (error): error is HttpError => error._tag === "HttpError", () => Effect.succeed("Recovering from HttpError") ) ) ``` ### catchTag Catches and handles specific errors by their `_tag` field, which is used as a discriminator. `Effect.catchTag` is useful when your errors are tagged with a `_tag` field that identifies the error type. You can use this function to handle specific error types by matching the `_tag` value. This allows for precise error handling, ensuring that only specific errors are caught and handled. The error type must have a `_tag` field to use `Effect.catchTag`. This field is used to identify and match errors. **Example** (Handling Errors by Tag) ```ts twoslash import { Effect, Random } from "effect" class HttpError { readonly _tag = "HttpError" } class ValidationError { readonly _tag = "ValidationError" } // ┌─── Effect // ▼ const program = Effect.gen(function* () { const n1 = yield* Random.next const n2 = yield* Random.next if (n1 < 0.5) { yield* Effect.fail(new HttpError()) } if (n2 < 0.5) { yield* Effect.fail(new ValidationError()) } return "some result" }) // ┌─── Effect // ▼ const recovered = program.pipe( // Only handle HttpError errors Effect.catchTag("HttpError", (_HttpError) => Effect.succeed("Recovering from HttpError") ) ) ``` In the example above, the `Effect.catchTag` function allows us to handle `HttpError` specifically. If a `HttpError` occurs during the execution of the program, the provided error handler function will be invoked, and the program will proceed with the recovery logic specified within the handler. We can observe that the type in the error channel of our program has changed to only show `ValidationError`: ```ts showLineNumbers=false const recovered: Effect ``` indicating that `HttpError` has been handled. If we also wanted to handle `ValidationError`, we can simply add another `catchTag`: **Example** (Handling Multiple Error Types with `catchTag`) ```ts twoslash collapse={3-23} {32-34} import { Effect, Random } from "effect" class HttpError { readonly _tag = "HttpError" } class ValidationError { readonly _tag = "ValidationError" } // ┌─── Effect // ▼ const program = Effect.gen(function* () { const n1 = yield* Random.next const n2 = yield* Random.next if (n1 < 0.5) { yield* Effect.fail(new HttpError()) } if (n2 < 0.5) { yield* Effect.fail(new ValidationError()) } return "some result" }) // ┌─── Effect // ▼ const recovered = program.pipe( // Handle both HttpError and ValidationError Effect.catchTag("HttpError", (_HttpError) => Effect.succeed("Recovering from HttpError") ), Effect.catchTag("ValidationError", (_ValidationError) => Effect.succeed("Recovering from ValidationError") ) ) ``` We can observe that the type in the error channel of our program has changed to `never`: ```ts showLineNumbers=false const recovered: Effect ``` indicating that all errors have been handled. ### catchTags Handles multiple errors in a single block of code using their `_tag` field. `Effect.catchTags` is a convenient way to handle multiple error types at once. Instead of using [`Effect.catchTag`](#catchtag) multiple times, you can pass an object where each key is an error type's `_tag`, and the value is the handler for that specific error. This allows you to catch and recover from multiple error types in a single call. **Example** (Handling Multiple Tagged Error Types at Once) ```ts twoslash import { Effect, Random } from "effect" class HttpError { readonly _tag = "HttpError" } class ValidationError { readonly _tag = "ValidationError" } // ┌─── Effect // ▼ const program = Effect.gen(function* () { const n1 = yield* Random.next const n2 = yield* Random.next if (n1 < 0.5) { yield* Effect.fail(new HttpError()) } if (n2 < 0.5) { yield* Effect.fail(new ValidationError()) } return "some result" }) // ┌─── Effect // ▼ const recovered = program.pipe( Effect.catchTags({ HttpError: (_HttpError) => Effect.succeed(`Recovering from HttpError`), ValidationError: (_ValidationError) => Effect.succeed(`Recovering from ValidationError`) }) ) ``` This function takes an object where each property represents a specific error `_tag` (`"HttpError"` and `"ValidationError"` in this case), and the corresponding value is the error handler function to be executed when that particular error occurs. ## Effect.fn The `Effect.fn` function allows you to create traced functions that return an effect. It provides two key features: - **Stack traces with location details** if an error occurs. - **Automatic span creation** for [tracing](/docs/observability/tracing/) when a span name is provided. If a span name is passed as the first argument, the function's execution is tracked using that name. If no name is provided, stack tracing still works, but spans are not created. A function can be defined using either: - A generator function, allowing the use of `yield*` for effect composition. - A regular function that returns an `Effect`. **Example** (Creating a Traced Function with a Span Name) ```ts twoslash import { Effect } from "effect" const myfunc = Effect.fn("myspan")(function* (n: N) { yield* Effect.annotateCurrentSpan("n", n) // Attach metadata to the span console.log(`got: ${n}`) yield* Effect.fail(new Error("Boom!")) // Simulate failure }) Effect.runFork(myfunc(100).pipe(Effect.catchAllCause(Effect.logError))) /* Output: got: 100 timestamp=... level=ERROR fiber=#0 cause="Error: Boom! at (/.../index.ts:6:22) <= Raise location at myspan (/.../index.ts:3:23) <= Definition location at myspan (/.../index.ts:9:16)" <= Call location */ ``` ### Exporting Spans for Tracing `Effect.fn` automatically creates [spans](/docs/observability/tracing/). The spans capture information about the function execution, including metadata and error details. **Example** (Exporting Spans to the Console) ```ts twoslash import { Effect } from "effect" import { NodeSdk } from "@effect/opentelemetry" import { ConsoleSpanExporter, BatchSpanProcessor } from "@opentelemetry/sdk-trace-base" const myfunc = Effect.fn("myspan")(function* (n: N) { yield* Effect.annotateCurrentSpan("n", n) console.log(`got: ${n}`) yield* Effect.fail(new Error("Boom!")) }) const program = myfunc(100) const NodeSdkLive = NodeSdk.layer(() => ({ resource: { serviceName: "example" }, // Export span data to the console spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter()) })) Effect.runFork(program.pipe(Effect.provide(NodeSdkLive))) /* Output: got: 100 { resource: { attributes: { 'service.name': 'example', 'telemetry.sdk.language': 'nodejs', 'telemetry.sdk.name': '@effect/opentelemetry', 'telemetry.sdk.version': '1.30.1' } }, instrumentationScope: { name: 'example', version: undefined, schemaUrl: undefined }, traceId: '22801570119e57a6e2aacda3dec9665b', parentId: undefined, traceState: undefined, name: 'myspan', id: '7af530c1e01bc0cb', kind: 0, timestamp: 1741182277518402.2, duration: 4300.416, attributes: { n: 100, 'code.stacktrace': 'at (/.../index.ts:8:23)\n' + 'at (/.../index.ts:14:17)' }, status: { code: 2, message: 'Boom!' }, events: [ { name: 'exception', attributes: { 'exception.type': 'Error', 'exception.message': 'Boom!', 'exception.stacktrace': 'Error: Boom!\n' + ' at (/.../index.ts:11:22)\n' + ' at myspan (/.../index.ts:8:23)\n' + ' at myspan (/.../index.ts:14:17)' }, time: [ 1741182277, 522702583 ], droppedAttributesCount: 0 } ], links: [] } */ ``` ### Using Effect.fn as a pipe Function `Effect.fn` also acts as a pipe function, allowing you to create a pipeline after the function definition using the effect returned by the generator function as the starting value of the pipeline. **Example** (Creating a Traced Function with a Delay) ```ts twoslash import { Effect } from "effect" const myfunc = Effect.fn( function* (n: number) { console.log(`got: ${n}`) yield* Effect.fail(new Error("Boom!")) }, // You can access both the created effect and the original arguments (effect, n) => Effect.delay(effect, `${n / 100} seconds`) ) Effect.runFork(myfunc(100).pipe(Effect.catchAllCause(Effect.logError))) /* Output: got: 100 timestamp=... level=ERROR fiber=#0 cause="Error: Boom! (<= after 1 second) */ ``` # [Fallback](https://effect.website/docs/error-management/fallback/) ## Overview import { Aside } from "@astrojs/starlight/components" This page explains various techniques for handling failures and creating fallback mechanisms in the Effect library. ## orElse `Effect.orElse` allows you to attempt to run an effect, and if it fails, you can provide a fallback effect to run instead. This is useful for handling failures gracefully by defining an alternative effect to execute if the first one encounters an error. **Example** (Handling Fallback with `Effect.orElse`) ```ts twoslash import { Effect } from "effect" const success = Effect.succeed("success") const failure = Effect.fail("failure") const fallback = Effect.succeed("fallback") // Try the success effect first, fallback is not used const program1 = Effect.orElse(success, () => fallback) console.log(Effect.runSync(program1)) // Output: "success" // Try the failure effect first, fallback is used const program2 = Effect.orElse(failure, () => fallback) console.log(Effect.runSync(program2)) // Output: "fallback" ``` ## orElseFail `Effect.orElseFail` allows you to replace the failure from one effect with a custom failure value. If the effect fails, you can provide a new failure to be returned instead of the original one. This function only applies to failed effects. If the effect succeeds, it will remain unaffected. **Example** (Replacing Failure with `Effect.orElseFail`) ```ts twoslash import { Effect } from "effect" const validate = (age: number): Effect.Effect => { if (age < 0) { return Effect.fail("NegativeAgeError") } else if (age < 18) { return Effect.fail("IllegalAgeError") } else { return Effect.succeed(age) } } const program = Effect.orElseFail(validate(-1), () => "invalid age") console.log(Effect.runSyncExit(program)) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'invalid age' } } */ ``` ## orElseSucceed `Effect.orElseSucceed` allows you to replace the failure of an effect with a success value. If the effect fails, it will instead succeed with the provided value, ensuring the effect always completes successfully. This is useful when you want to guarantee a successful result regardless of whether the original effect failed. The function ensures that any failure is effectively "swallowed" and replaced by a successful value, which can be helpful for providing default values in case of failure. This function only applies to failed effects. If the effect already succeeds, it will remain unchanged. **Example** (Replacing Failure with Success using `Effect.orElseSucceed`) ```ts twoslash import { Effect } from "effect" const validate = (age: number): Effect.Effect => { if (age < 0) { return Effect.fail("NegativeAgeError") } else if (age < 18) { return Effect.fail("IllegalAgeError") } else { return Effect.succeed(age) } } const program = Effect.orElseSucceed(validate(-1), () => 18) console.log(Effect.runSyncExit(program)) /* Output: { _id: 'Exit', _tag: 'Success', value: 18 } */ ``` ## firstSuccessOf `Effect.firstSuccessOf` allows you to try multiple effects in sequence, and as soon as one of them succeeds, it returns that result. If all effects fail, it returns the error of the last effect in the list. This is useful when you have several potential alternatives and want to use the first one that works. This function is sequential, meaning that the `Effect` values in the iterable will be executed in sequence, and the first one that succeeds will determine the outcome of the resulting `Effect` value. **Example** (Finding Configuration with Fallbacks) In this example, we try to retrieve a configuration from different nodes. If the primary node fails, we fall back to other nodes until we find a successful configuration. ```ts twoslash import { Effect, Console } from "effect" interface Config { host: string port: number apiKey: string } // Create a configuration object with sample values const makeConfig = (name: string): Config => ({ host: `${name}.example.com`, port: 8080, apiKey: "12345-abcde" }) // Simulate retrieving configuration from a remote node const remoteConfig = (name: string): Effect.Effect => Effect.gen(function* () { // Simulate node3 being the only one with available config if (name === "node3") { yield* Console.log(`Config for ${name} found`) return makeConfig(name) } else { yield* Console.log(`Unavailable config for ${name}`) return yield* Effect.fail(new Error(`Config not found for ${name}`)) } }) // Define the master configuration and potential fallback nodes const masterConfig = remoteConfig("master") const nodeConfigs = ["node1", "node2", "node3", "node4"].map(remoteConfig) // Attempt to find a working configuration, // starting with the master and then falling back to other nodes const config = Effect.firstSuccessOf([masterConfig, ...nodeConfigs]) // Run the effect to retrieve the configuration const result = Effect.runSync(config) console.log(result) /* Output: Unavailable config for master Unavailable config for node1 Unavailable config for node2 Config for node3 found { host: 'node3.example.com', port: 8080, apiKey: '12345-abcde' } */ ``` # [Matching](https://effect.website/docs/error-management/matching/) ## Overview import { Aside } from "@astrojs/starlight/components" In the Effect module, similar to other modules like [Option](/docs/data-types/option/#pattern-matching) and [Exit](/docs/data-types/exit/#pattern-matching), we have a `Effect.match` function that allows us to handle different cases simultaneously. Additionally, Effect provides various functions to manage both success and failure scenarios in effectful programs. ## match `Effect.match` lets you define custom handlers for both success and failure scenarios. You provide separate functions to handle each case, allowing you to process the result if the effect succeeds, or handle the error if the effect fails. This is useful for structuring your code to respond differently to success or failure without triggering side effects. **Example** (Handling Both Success and Failure Cases) ```ts twoslash import { Effect } from "effect" const success: Effect.Effect = Effect.succeed(42) const program1 = Effect.match(success, { onFailure: (error) => `failure: ${error.message}`, onSuccess: (value) => `success: ${value}` }) // Run and log the result of the successful effect Effect.runPromise(program1).then(console.log) // Output: "success: 42" const failure: Effect.Effect = Effect.fail( new Error("Uh oh!") ) const program2 = Effect.match(failure, { onFailure: (error) => `failure: ${error.message}`, onSuccess: (value) => `success: ${value}` }) // Run and log the result of the failed effect Effect.runPromise(program2).then(console.log) // Output: "failure: Uh oh!" ``` ## ignore `Effect.ignore` allows you to run an effect without caring about its result, whether it succeeds or fails. This is useful when you only care about the side effects of the effect and do not need to handle or process its outcome. **Example** (Using `Effect.ignore` to Discard Values) ```ts twoslash import { Effect } from "effect" // ┌─── Effect // ▼ const task = Effect.fail("Uh oh!").pipe(Effect.as(5)) // ┌─── Effect // ▼ const program = Effect.ignore(task) ``` ## matchEffect The `Effect.matchEffect` function is similar to [Effect.match](#match), but it enables you to perform side effects in the handlers for both success and failure outcomes. This is useful when you need to execute additional actions, like logging or notifying users, based on whether an effect succeeds or fails. **Example** (Handling Success and Failure with Side Effects) ```ts twoslash import { Effect } from "effect" const success: Effect.Effect = Effect.succeed(42) const failure: Effect.Effect = Effect.fail( new Error("Uh oh!") ) const program1 = Effect.matchEffect(success, { onFailure: (error) => Effect.succeed(`failure: ${error.message}`).pipe( Effect.tap(Effect.log) ), onSuccess: (value) => Effect.succeed(`success: ${value}`).pipe(Effect.tap(Effect.log)) }) console.log(Effect.runSync(program1)) /* Output: timestamp=... level=INFO fiber=#0 message="success: 42" success: 42 */ const program2 = Effect.matchEffect(failure, { onFailure: (error) => Effect.succeed(`failure: ${error.message}`).pipe( Effect.tap(Effect.log) ), onSuccess: (value) => Effect.succeed(`success: ${value}`).pipe(Effect.tap(Effect.log)) }) console.log(Effect.runSync(program2)) /* Output: timestamp=... level=INFO fiber=#1 message="failure: Uh oh!" failure: Uh oh! */ ``` ## matchCause The `Effect.matchCause` function allows you to handle failures with access to the full [cause](/docs/data-types/cause/) of the failure within a fiber. This is useful for differentiating between different types of errors, such as regular failures, defects, or interruptions. You can provide specific handling logic for each failure type based on the cause. **Example** (Handling Different Failure Causes) ```ts twoslash import { Effect } from "effect" const task: Effect.Effect = Effect.die("Uh oh!") const program = Effect.matchCause(task, { onFailure: (cause) => { switch (cause._tag) { case "Fail": // Handle standard failure return `Fail: ${cause.error.message}` case "Die": // Handle defects (unexpected errors) return `Die: ${cause.defect}` case "Interrupt": // Handle interruption return `${cause.fiberId} interrupted!` } // Fallback for other causes return "failed due to other causes" }, onSuccess: (value) => // task completes successfully `succeeded with ${value} value` }) Effect.runPromise(program).then(console.log) // Output: "Die: Uh oh!" ``` ## matchCauseEffect The `Effect.matchCauseEffect` function works similarly to [Effect.matchCause](#matchcause), but it also allows you to perform additional side effects based on the failure cause. This function provides access to the complete [cause](/docs/data-types/cause/) of the failure, making it possible to differentiate between various failure types, and allows you to respond accordingly while performing side effects (like logging or other operations). **Example** (Handling Different Failure Causes with Side Effects) ```ts twoslash import { Effect, Console } from "effect" const task: Effect.Effect = Effect.die("Uh oh!") const program = Effect.matchCauseEffect(task, { onFailure: (cause) => { switch (cause._tag) { case "Fail": // Handle standard failure with a logged message return Console.log(`Fail: ${cause.error.message}`) case "Die": // Handle defects (unexpected errors) by logging the defect return Console.log(`Die: ${cause.defect}`) case "Interrupt": // Handle interruption and log the fiberId that was interrupted return Console.log(`${cause.fiberId} interrupted!`) } // Fallback for other causes return Console.log("failed due to other causes") }, onSuccess: (value) => // Log success if the task completes successfully Console.log(`succeeded with ${value} value`) }) Effect.runPromise(program) // Output: "Die: Uh oh!" ``` # [Retrying](https://effect.website/docs/error-management/retrying/) ## Overview import { Aside } from "@astrojs/starlight/components" In software development, it's common to encounter situations where an operation may fail temporarily due to various factors such as network issues, resource unavailability, or external dependencies. In such cases, it's often desirable to retry the operation automatically, allowing it to succeed eventually. Retrying is a powerful mechanism to handle transient failures and ensure the successful execution of critical operations. In Effect retrying is made simple and flexible with built-in functions and scheduling strategies. In this guide, we will explore the concept of retrying in Effect and learn how to use the `retry` and `retryOrElse` functions to handle failure scenarios. We'll see how to define retry policies using schedules, which dictate when and how many times the operation should be retried. Whether you're working on network requests, database interactions, or any other potentially error-prone operations, mastering the retrying capabilities of effect can significantly enhance the resilience and reliability of your applications. ## retry The `Effect.retry` function takes an effect and a [Schedule](/docs/scheduling/introduction/) policy, and will automatically retry the effect if it fails, following the rules of the policy. If the effect ultimately succeeds, the result will be returned. If the maximum retries are exhausted and the effect still fails, the failure is propagated. This can be useful when dealing with intermittent failures, such as network issues or temporary resource unavailability. By defining a retry policy, you can control the number of retries, the delay between them, and when to stop retrying. **Example** (Retrying with a Fixed Delay) ```ts twoslash import { Effect, Schedule } from "effect" let count = 0 // Simulates an effect with possible failures const task = Effect.async((resume) => { if (count <= 2) { count++ console.log("failure") resume(Effect.fail(new Error())) } else { console.log("success") resume(Effect.succeed("yay!")) } }) // Define a repetition policy using a fixed delay between retries const policy = Schedule.fixed("100 millis") const repeated = Effect.retry(task, policy) Effect.runPromise(repeated).then(console.log) /* Output: failure failure failure success yay! */ ``` ### Retrying n Times Immediately You can also retry a failing effect a set number of times with a simpler policy that retries immediately: **Example** (Retrying a Task up to 5 times) ```ts twoslash import { Effect } from "effect" let count = 0 // Simulates an effect with possible failures const task = Effect.async((resume) => { if (count <= 2) { count++ console.log("failure") resume(Effect.fail(new Error())) } else { console.log("success") resume(Effect.succeed("yay!")) } }) // Retry the task up to 5 times Effect.runPromise(Effect.retry(task, { times: 5 })) /* Output: failure failure failure success */ ``` ### Retrying Based on a Condition You can customize how retries are managed by specifying conditions. Use the `until` or `while` options to control when retries stop. **Example** (Retrying Until a Specific Condition is Met) ```ts twoslash import { Effect } from "effect" let count = 0 // Define an effect that simulates varying error on each invocation const action = Effect.failSync(() => { console.log(`Action called ${++count} time(s)`) return `Error ${count}` }) // Retry the action until a specific condition is met const program = Effect.retry(action, { until: (err) => err === "Error 3" }) Effect.runPromiseExit(program).then(console.log) /* Output: Action called 1 time(s) Action called 2 time(s) Action called 3 time(s) { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Error 3' } } */ ``` ## retryOrElse The `Effect.retryOrElse` function attempts to retry a failing effect multiple times according to a defined [Schedule](/docs/scheduling/introduction/) policy. If the retries are exhausted and the effect still fails, it runs a fallback effect instead. This function is useful when you want to handle failures gracefully by specifying an alternative action after repeated failures. **Example** (Retrying with Fallback) ```ts twoslash import { Effect, Schedule, Console } from "effect" let count = 0 // Simulates an effect with possible failures const task = Effect.async((resume) => { if (count <= 2) { count++ console.log("failure") resume(Effect.fail(new Error())) } else { console.log("success") resume(Effect.succeed("yay!")) } }) // Retry the task with a delay between retries and a maximum of 2 retries const policy = Schedule.addDelay(Schedule.recurs(2), () => "100 millis") // If all retries fail, run the fallback effect const repeated = Effect.retryOrElse( task, policy, // fallback () => Console.log("orElse").pipe(Effect.as("default value")) ) Effect.runPromise(repeated).then(console.log) /* Output: failure failure failure orElse default value */ ``` # [Parallel and Sequential Errors](https://effect.website/docs/error-management/parallel-and-sequential-errors/) ## Overview import { Aside } from "@astrojs/starlight/components" When working with Effect, if an error occurs, the default behavior is to fail with the first error encountered. **Example** (Failing on the First Error) Here, the program fails with the first error it encounters, `"Oh uh!"`. ```ts twoslash import { Effect } from "effect" const fail = Effect.fail("Oh uh!") const die = Effect.dieMessage("Boom!") // Run both effects sequentially const program = Effect.all([fail, die]) Effect.runPromiseExit(program).then(console.log) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Oh uh!' } } */ ``` ## Parallel Errors In some cases, you might encounter multiple errors, especially during concurrent computations. When tasks are run concurrently, multiple errors can happen at the same time. **Example** (Handling Multiple Errors in Concurrent Computations) In this example, both the `fail` and `die` effects are executed concurrently. Since both fail, the program will report multiple errors in the output. ```ts twoslash import { Effect } from "effect" const fail = Effect.fail("Oh uh!") const die = Effect.dieMessage("Boom!") // Run both effects concurrently const program = Effect.all([fail, die], { concurrency: "unbounded" }).pipe(Effect.asVoid) Effect.runPromiseExit(program).then(console.log) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Parallel', left: { _id: 'Cause', _tag: 'Fail', failure: 'Oh uh!' }, right: { _id: 'Cause', _tag: 'Die', defect: [Object] } } } */ ``` ### parallelErrors Effect provides a function called `Effect.parallelErrors` that captures all failure errors from concurrent operations in the error channel. **Example** (Capturing Multiple Concurrent Failures) In this example, `Effect.parallelErrors` combines the errors from `fail1` and `fail2` into a single error. ```ts twoslash import { Effect } from "effect" const fail1 = Effect.fail("Oh uh!") const fail2 = Effect.fail("Oh no!") const die = Effect.dieMessage("Boom!") // Run all effects concurrently and capture all errors const program = Effect.all([fail1, fail2, die], { concurrency: "unbounded" }).pipe(Effect.asVoid, Effect.parallelErrors) Effect.runPromiseExit(program).then(console.log) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: [ 'Oh uh!', 'Oh no!' ] } } */ ``` ## Sequential Errors When working with resource-safety operators like `Effect.ensuring`, you may encounter multiple sequential errors. This happens because regardless of whether the original effect has any errors or not, the finalizer is uninterruptible and will always run. **Example** (Handling Multiple Sequential Errors) In this example, both `fail` and the finalizer `die` result in sequential errors, and both are captured. ```ts twoslash import { Effect } from "effect" // Simulate an effect that fails const fail = Effect.fail("Oh uh!") // Simulate a finalizer that causes a defect const die = Effect.dieMessage("Boom!") // The finalizer 'die' will always run, even if 'fail' fails const program = fail.pipe(Effect.ensuring(die)) Effect.runPromiseExit(program).then(console.log) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Sequential', left: { _id: 'Cause', _tag: 'Fail', failure: 'Oh uh!' }, right: { _id: 'Cause', _tag: 'Die', defect: [Object] } } } */ ``` # [Sandboxing](https://effect.website/docs/error-management/sandboxing/) ## Overview Errors are an inevitable part of programming, and they can arise from various sources like failures, defects, fiber interruptions, or combinations of these. This guide explains how to use the `Effect.sandbox` function to isolate and understand the causes of errors in your Effect-based code. ## sandbox / unsandbox The `Effect.sandbox` function allows you to encapsulate all the potential causes of an error in an effect. It exposes the full cause of an effect, whether it's due to a failure, defect, fiber interruption, or a combination of these factors. In simple terms, it takes an effect `Effect` and transforms it into an effect `Effect, R>` where the error channel now contains a detailed cause of the error. **Syntax** ```ts showLineNumbers=false Effect -> Effect, R> ``` By using the `Effect.sandbox` function, you gain access to the underlying causes of exceptional effects. These causes are represented as a type of `Cause` and are available in the error channel of the `Effect` data type. Once you have exposed the causes, you can utilize standard error-handling operators like [Effect.catchAll](/docs/error-management/expected-errors/#catchall) and [Effect.catchTags](/docs/error-management/expected-errors/#catchtags) to handle errors more effectively. These operators allow you to respond to specific error conditions. If needed, we can undo the sandboxing operation with `Effect.unsandbox`. **Example** (Handling Different Error Causes) ```ts twoslash import { Effect, Console } from "effect" // ┌─── Effect // ▼ const task = Effect.fail(new Error("Oh uh!")).pipe( Effect.as("primary result") ) // ┌─── Effect, never> // ▼ const sandboxed = Effect.sandbox(task) const program = Effect.catchTags(sandboxed, { Die: (cause) => Console.log(`Caught a defect: ${cause.defect}`).pipe( Effect.as("fallback result on defect") ), Interrupt: (cause) => Console.log(`Caught a defect: ${cause.fiberId}`).pipe( Effect.as("fallback result on fiber interruption") ), Fail: (cause) => Console.log(`Caught a defect: ${cause.error}`).pipe( Effect.as("fallback result on failure") ) }) // Restore the original error handling with unsandbox const main = Effect.unsandbox(program) Effect.runPromise(main).then(console.log) /* Output: Caught a defect: Oh uh! fallback result on failure */ ``` # [Timing Out](https://effect.website/docs/error-management/timing-out/) ## Overview import { Aside } from "@astrojs/starlight/components" In programming, it's common to deal with tasks that may take some time to complete. Often, we want to enforce a limit on how long we're willing to wait for these tasks. The `Effect.timeout` function helps by placing a time constraint on an operation, ensuring it doesn't run indefinitely. ## Basic Usage ### timeout The `Effect.timeout` function employs a [Duration](/docs/data-types/duration/) parameter to establish a time limit on an operation. If the operation exceeds this limit, a `TimeoutException` is triggered, indicating a timeout has occurred. **Example** (Setting a Timeout) Here, the task completes within the timeout duration, so the result is returned successfully. ```ts twoslash import { Effect } from "effect" const task = Effect.gen(function* () { console.log("Start processing...") yield* Effect.sleep("2 seconds") // Simulates a delay in processing console.log("Processing complete.") return "Result" }) // Sets a 3-second timeout for the task const timedEffect = task.pipe(Effect.timeout("3 seconds")) // Output will show that the task completes successfully // as it falls within the timeout duration Effect.runPromiseExit(timedEffect).then(console.log) /* Output: Start processing... Processing complete. { _id: 'Exit', _tag: 'Success', value: 'Result' } */ ``` If the operation exceeds the specified duration, a `TimeoutException` is raised: ```ts twoslash import { Effect } from "effect" const task = Effect.gen(function* () { console.log("Start processing...") yield* Effect.sleep("2 seconds") // Simulates a delay in processing console.log("Processing complete.") return "Result" }) // Output will show a TimeoutException as the task takes longer // than the specified timeout duration const timedEffect = task.pipe(Effect.timeout("1 second")) Effect.runPromiseExit(timedEffect).then(console.log) /* Output: Start processing... { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: { _tag: 'TimeoutException' } } } */ ``` ### timeoutOption If you want to handle timeouts more gracefully, consider using `Effect.timeoutOption`. This function treats timeouts as regular results, wrapping the outcome in an [Option](/docs/data-types/option/). **Example** (Handling Timeout as an Option) In this example, the first task completes successfully, while the second times out. The result of the timed-out task is represented as `None` in the `Option` type. ```ts twoslash import { Effect } from "effect" const task = Effect.gen(function* () { console.log("Start processing...") yield* Effect.sleep("2 seconds") // Simulates a delay in processing console.log("Processing complete.") return "Result" }) const timedOutEffect = Effect.all([ task.pipe(Effect.timeoutOption("3 seconds")), task.pipe(Effect.timeoutOption("1 second")) ]) Effect.runPromise(timedOutEffect).then(console.log) /* Output: Start processing... Processing complete. Start processing... [ { _id: 'Option', _tag: 'Some', value: 'Result' }, { _id: 'Option', _tag: 'None' } ] */ ``` ## Handling Timeouts When an operation does not finish within the specified duration, the behavior of the `Effect.timeout` depends on whether the operation is "uninterruptible". 1. **Interruptible Operation**: If the operation can be interrupted, it is terminated immediately once the timeout threshold is reached, resulting in a `TimeoutException`. ```ts twoslash import { Effect } from "effect" const task = Effect.gen(function* () { console.log("Start processing...") yield* Effect.sleep("2 seconds") // Simulates a delay in processing console.log("Processing complete.") return "Result" }) const timedEffect = task.pipe(Effect.timeout("1 second")) Effect.runPromiseExit(timedEffect).then(console.log) /* Output: Start processing... { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: { _tag: 'TimeoutException' } } } */ ``` 2. **Uninterruptible Operation**: If the operation is uninterruptible, it continues until completion before the `TimeoutException` is assessed. ```ts twoslash import { Effect } from "effect" const task = Effect.gen(function* () { console.log("Start processing...") yield* Effect.sleep("2 seconds") // Simulates a delay in processing console.log("Processing complete.") return "Result" }) const timedEffect = task.pipe( Effect.uninterruptible, Effect.timeout("1 second") ) // Outputs a TimeoutException after the task completes, // because the task is uninterruptible Effect.runPromiseExit(timedEffect).then(console.log) /* Output: Start processing... Processing complete. { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: { _tag: 'TimeoutException' } } } */ ``` ## Disconnection on Timeout The `Effect.disconnect` function provides a way to handle timeouts in uninterruptible effects more flexibly. It allows an uninterruptible effect to complete in the background, while the main control flow proceeds as if a timeout had occurred. Here's the distinction: **Without** `Effect.disconnect`: - An uninterruptible effect will ignore the timeout and continue executing until it completes, after which the timeout error is assessed. - This can lead to delays in recognizing a timeout condition because the system must wait for the effect to complete. **With** `Effect.disconnect`: - The uninterruptible effect is allowed to continue in the background, independent of the main control flow. - The main control flow recognizes the timeout immediately and proceeds with the timeout error or alternative logic, without having to wait for the effect to complete. - This method is particularly useful when the operations of the effect do not need to block the continuation of the program, despite being marked as uninterruptible. **Example** (Running Uninterruptible Tasks with Timeout and Background Completion) Consider a scenario where a long-running data processing task is initiated, and you want to ensure the system remains responsive, even if the data processing takes too long: ```ts twoslash import { Effect } from "effect" const longRunningTask = Effect.gen(function* () { console.log("Start heavy processing...") yield* Effect.sleep("5 seconds") // Simulate a long process console.log("Heavy processing done.") return "Data processed" }) const timedEffect = longRunningTask.pipe( Effect.uninterruptible, // Allows the task to finish in the background if it times out Effect.disconnect, Effect.timeout("1 second") ) Effect.runPromiseExit(timedEffect).then(console.log) /* Output: Start heavy processing... { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: { _tag: 'TimeoutException' } } } Heavy processing done. */ ``` In this example, the system detects the timeout after one second, but the long-running task continues and completes in the background, without blocking the program's flow. ## Customizing Timeout Behavior In addition to the basic `Effect.timeout` function, there are variations available that allow you to customize the behavior when a timeout occurs. ### timeoutFail The `Effect.timeoutFail` function allows you to produce a specific error when a timeout happens. **Example** (Custom Timeout Error) ```ts twoslash import { Effect } from "effect" const task = Effect.gen(function* () { console.log("Start processing...") yield* Effect.sleep("2 seconds") // Simulates a delay in processing console.log("Processing complete.") return "Result" }) class MyTimeoutError { readonly _tag = "MyTimeoutError" } const program = task.pipe( Effect.timeoutFail({ duration: "1 second", onTimeout: () => new MyTimeoutError() // Custom timeout error }) ) Effect.runPromiseExit(program).then(console.log) /* Output: Start processing... { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: MyTimeoutError { _tag: 'MyTimeoutError' } } } */ ``` ### timeoutFailCause `Effect.timeoutFailCause` lets you define a specific defect to throw when a timeout occurs. This is helpful for treating timeouts as exceptional cases in your code. **Example** (Custom Defect on Timeout) ```ts twoslash import { Effect, Cause } from "effect" const task = Effect.gen(function* () { console.log("Start processing...") yield* Effect.sleep("2 seconds") // Simulates a delay in processing console.log("Processing complete.") return "Result" }) const program = task.pipe( Effect.timeoutFailCause({ duration: "1 second", onTimeout: () => Cause.die("Timed out!") // Custom defect for timeout }) ) Effect.runPromiseExit(program).then(console.log) /* Output: Start processing... { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Die', defect: 'Timed out!' } } */ ``` ### timeoutTo `Effect.timeoutTo` provides more flexibility compared to `Effect.timeout`, allowing you to define different outcomes for both successful and timed-out operations. This can be useful when you want to customize the result based on whether the operation completes in time or not. **Example** (Handling Success and Timeout with [Either](/docs/data-types/either/)) ```ts twoslash import { Effect, Either } from "effect" const task = Effect.gen(function* () { console.log("Start processing...") yield* Effect.sleep("2 seconds") // Simulates a delay in processing console.log("Processing complete.") return "Result" }) const program = task.pipe( Effect.timeoutTo({ duration: "1 second", onSuccess: (result): Either.Either => Either.right(result), onTimeout: (): Either.Either => Either.left("Timed out!") }) ) Effect.runPromise(program).then(console.log) /* Output: Start processing... { _id: "Either", _tag: "Left", left: "Timed out!" } */ ``` # [Two Types of Errors](https://effect.website/docs/error-management/two-error-types/) ## Overview Just like any other program, Effect programs may fail for expected or unexpected reasons. The difference between a non-Effect program and an Effect program is in the detail provided to you when your program fails. Effect attempts to preserve as much information as possible about what caused your program to fail to produce a detailed, comprehensive, and human readable failure message. In an Effect program, there are two possible ways for a program to fail: - **Expected Errors**: These are errors that developers anticipate and expect as part of normal program execution. - **Unexpected Errors**: These are errors that occur unexpectedly and are not part of the intended program flow. ## Expected Errors These errors, also referred to as _failures_, _typed errors_ or _recoverable errors_, are errors that developers anticipate as part of the normal program execution. They serve a similar purpose to checked exceptions and play a role in defining the program's domain and control flow. Expected errors **are tracked** at the type level by the `Effect` data type in the "Error" channel: ```ts "HttpError" showLineNumbers=false const program: Effect ``` it is evident from the type that the program can fail with an error of type `HttpError`. ## Unexpected Errors Unexpected errors, also referred to as _defects_, _untyped errors_, or _unrecoverable errors_, are errors that developers do not anticipate occurring during normal program execution. Unlike expected errors, which are considered part of a program's domain and control flow, unexpected errors resemble unchecked exceptions and lie outside the expected behavior of the program. Since these errors are not expected, Effect **does not track** them at the type level. However, the Effect runtime does keep track of these errors and provides several methods to aid in recovering from unexpected errors. # [Unexpected Errors](https://effect.website/docs/error-management/unexpected-errors/) ## Overview import { Aside } from "@astrojs/starlight/components" There are situations where you may encounter unexpected errors, and you need to decide how to handle them. Effect provides functions to help you deal with such scenarios, allowing you to take appropriate actions when errors occur during the execution of your effects. ## Creating Unrecoverable Errors In the same way it is possible to leverage combinators such as [Effect.fail](/docs/getting-started/creating-effects/#fail) to create values of type `Effect` the Effect library provides tools to create defects. Creating defects is a common necessity when dealing with errors from which it is not possible to recover from a business logic perspective, such as attempting to establish a connection that is refused after multiple retries. In those cases terminating the execution of the effect and moving into reporting, through an output such as stdout or some external monitoring service, might be the best solution. The following functions and combinators allow for termination of the effect and are often used to convert values of type `Effect` into values of type `Effect` allowing the programmer an escape hatch from having to handle and recover from errors for which there is no sensible way to recover. ### die Creates an effect that terminates a fiber with a specified error. Use `Effect.die` when encountering unexpected conditions in your code that should not be handled as regular errors but instead represent unrecoverable defects. The `Effect.die` function is used to signal a defect, which represents a critical and unexpected error in the code. When invoked, it produces an effect that does not handle the error and instead terminates the fiber. The error channel of the resulting effect is of type `never`, indicating that it cannot recover from this failure. **Example** (Terminating on Division by Zero with a Specified Error) ```ts twoslash import { Effect } from "effect" const divide = (a: number, b: number) => b === 0 ? Effect.die(new Error("Cannot divide by zero")) : Effect.succeed(a / b) // ┌─── Effect // ▼ const program = divide(1, 0) Effect.runPromise(program).catch(console.error) /* Output: (FiberFailure) Error: Cannot divide by zero ...stack trace... */ ``` ### dieMessage Creates an effect that terminates a fiber with a `RuntimeException` containing the specified message. Use `Effect.dieMessage` when you want to terminate a fiber due to an unrecoverable defect and include a clear explanation in the message. The `Effect.dieMessage` function is used to signal a defect, representing a critical and unexpected error in the code. When invoked, it produces an effect that terminates the fiber with a `RuntimeException` carrying the given message. The resulting effect has an error channel of type `never`, indicating it does not handle or recover from the error. **Example** (Terminating on Division by Zero with a Specified Message) ```ts twoslash import { Effect } from "effect" const divide = (a: number, b: number) => b === 0 ? Effect.dieMessage("Cannot divide by zero") : Effect.succeed(a / b) // ┌─── Effect // ▼ const program = divide(1, 0) Effect.runPromise(program).catch(console.error) /* Output: (FiberFailure) RuntimeException: Cannot divide by zero ...stack trace... */ ``` ## Converting Failures to Defects ### orDie Converts an effect's failure into a fiber termination, removing the error from the effect's type. Use `Effect.orDie` when failures should be treated as unrecoverable defects and no error handling is required. The `Effect.orDie` function is used when you encounter errors that you do not want to handle or recover from. It removes the error type from the effect and ensures that any failure will terminate the fiber. This is useful for propagating failures as defects, signaling that they should not be handled within the effect. **Example** (Propagating an Error as a Defect) ```ts twoslash import { Effect } from "effect" const divide = (a: number, b: number) => b === 0 ? Effect.fail(new Error("Cannot divide by zero")) : Effect.succeed(a / b) // ┌─── Effect // ▼ const program = Effect.orDie(divide(1, 0)) Effect.runPromise(program).catch(console.error) /* Output: (FiberFailure) Error: Cannot divide by zero ...stack trace... */ ``` ### orDieWith Converts an effect's failure into a fiber termination with a custom error. Use `Effect.orDieWith` when failures should terminate the fiber as defects, and you want to customize the error for clarity or debugging purposes. The `Effect.orDieWith` function behaves like [Effect.orDie](#ordie), but it allows you to provide a mapping function to transform the error before terminating the fiber. This is useful for cases where you want to include a more detailed or user-friendly error when the failure is propagated as a defect. **Example** (Customizing Defect) ```ts twoslash import { Effect } from "effect" const divide = (a: number, b: number) => b === 0 ? Effect.fail(new Error("Cannot divide by zero")) : Effect.succeed(a / b) // ┌─── Effect // ▼ const program = Effect.orDieWith( divide(1, 0), (error) => new Error(`defect: ${error.message}`) ) Effect.runPromise(program).catch(console.error) /* Output: (FiberFailure) Error: defect: Cannot divide by zero ...stack trace... */ ``` ## Catching All Defects There is no sensible way to recover from defects. The functions we're about to discuss should be used only at the boundary between Effect and an external system, to transmit information on a defect for diagnostic or explanatory purposes. ### exit The `Effect.exit` function transforms an `Effect` into an effect that encapsulates both potential failure and success within an [Exit](/docs/data-types/exit/) data type: ```ts showLineNumbers=false Effect -> Effect, never, R> ``` This means if you have an effect with the following type: ```ts showLineNumbers=false Effect ``` and you call `Effect.exit` on it, the type becomes: ```ts showLineNumbers=false Effect, never, never> ``` The resulting effect cannot fail because the potential failure is now represented within the `Exit`'s `Failure` type. The error type of the returned effect is specified as `never`, confirming that the effect is structured to not fail. By yielding an `Exit`, we gain the ability to "pattern match" on this type to handle both failure and success cases within the generator function. **Example** (Catching Defects with `Effect.exit`) ```ts twoslash import { Effect, Cause, Console, Exit } from "effect" // Simulating a runtime error const task = Effect.dieMessage("Boom!") const program = Effect.gen(function* () { const exit = yield* Effect.exit(task) if (Exit.isFailure(exit)) { const cause = exit.cause if ( Cause.isDieType(cause) && Cause.isRuntimeException(cause.defect) ) { yield* Console.log( `RuntimeException defect caught: ${cause.defect.message}` ) } else { yield* Console.log("Unknown failure caught.") } } }) // We get an Exit.Success because we caught all failures Effect.runPromiseExit(program).then(console.log) /* Output: RuntimeException defect caught: Boom! { _id: "Exit", _tag: "Success", value: undefined } */ ``` ### catchAllDefect Recovers from all defects using a provided recovery function. `Effect.catchAllDefect` allows you to handle defects, which are unexpected errors that usually cause the program to terminate. This function lets you recover from these defects by providing a function that handles the error. However, it does not handle expected errors (like those from [Effect.fail](/docs/getting-started/creating-effects/#fail)) or execution interruptions (like those from [Effect.interrupt](/docs/concurrency/basic-concurrency/#interrupt)). **Example** (Handling All Defects) ```ts twoslash import { Effect, Cause, Console } from "effect" // Simulating a runtime error const task = Effect.dieMessage("Boom!") const program = Effect.catchAllDefect(task, (defect) => { if (Cause.isRuntimeException(defect)) { return Console.log( `RuntimeException defect caught: ${defect.message}` ) } return Console.log("Unknown defect caught.") }) // We get an Exit.Success because we caught all defects Effect.runPromiseExit(program).then(console.log) /* Output: RuntimeException defect caught: Boom! { _id: "Exit", _tag: "Success", value: undefined } */ ``` ## Catching Some Defects ### catchSomeDefect Recovers from specific defects using a provided partial function. `Effect.catchSomeDefect` allows you to handle specific defects, which are unexpected errors that can cause the program to stop. It uses a partial function to catch only certain defects and ignores others. However, it does not handle expected errors (like those from [Effect.fail](/docs/getting-started/creating-effects/#fail)) or execution interruptions (like those from [Effect.interrupt](/docs/concurrency/basic-concurrency/#interrupt)). The function provided to `Effect.catchSomeDefect` acts as a filter and a handler for defects: - It receives the defect as an input. - If the defect matches a specific condition (e.g., a certain error type), the function returns an `Option.some` containing the recovery logic. - If the defect does not match, the function returns `Option.none`, allowing the defect to propagate. **Example** (Handling Specific Defects) ```ts twoslash import { Effect, Cause, Option, Console } from "effect" // Simulating a runtime error const task = Effect.dieMessage("Boom!") const program = Effect.catchSomeDefect(task, (defect) => { if (Cause.isIllegalArgumentException(defect)) { return Option.some( Console.log( `Caught an IllegalArgumentException defect: ${defect.message}` ) ) } return Option.none() }) // Since we are only catching IllegalArgumentException // we will get an Exit.Failure because we simulated a runtime error. Effect.runPromiseExit(program).then(console.log) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Die', defect: { _tag: 'RuntimeException' } } } */ ``` # [Yieldable Errors](https://effect.website/docs/error-management/yieldable-errors/) ## Overview Yieldable Errors are special types of errors that can be yielded directly within a generator function using [Effect.gen](/docs/getting-started/using-generators/). These errors allow you to handle them intuitively, without needing to explicitly invoke [Effect.fail](/docs/getting-started/creating-effects/#fail). This simplifies how you manage custom errors in your code. ## Data.Error The `Data.Error` constructor provides a way to define a base class for yieldable errors. **Example** (Creating and Yielding a Custom Error) ```ts twoslash import { Effect, Data } from "effect" // Define a custom error class extending Data.Error class MyError extends Data.Error<{ message: string }> {} export const program = Effect.gen(function* () { // Yield a custom error (equivalent to failing with MyError) yield* new MyError({ message: "Oh no!" }) }) Effect.runPromiseExit(program).then(console.log) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: { message: 'Oh no!' } } } */ ``` ## Data.TaggedError The `Data.TaggedError` constructor lets you define custom yieldable errors with unique tags. Each error has a `_tag` property, allowing you to easily distinguish between different error types. This makes it convenient to handle specific tagged errors using functions like [Effect.catchTag](/docs/error-management/expected-errors/#catchtag) or [Effect.catchTags](/docs/error-management/expected-errors/#catchtags). **Example** (Handling Multiple Tagged Errors) ```ts twoslash import { Effect, Data, Random } from "effect" // An error with _tag: "Foo" class FooError extends Data.TaggedError("Foo")<{ message: string }> {} // An error with _tag: "Bar" class BarError extends Data.TaggedError("Bar")<{ randomNumber: number }> {} const program = Effect.gen(function* () { const n = yield* Random.next return n > 0.5 ? "yay!" : n < 0.2 ? yield* new FooError({ message: "Oh no!" }) : yield* new BarError({ randomNumber: n }) }).pipe( // Handle different tagged errors using catchTags Effect.catchTags({ Foo: (error) => Effect.succeed(`Foo error: ${error.message}`), Bar: (error) => Effect.succeed(`Bar error: ${error.randomNumber}`) }) ) Effect.runPromise(program).then(console.log, console.error) /* Example Output (n < 0.2): Foo error: Oh no! */ ``` # [Building Pipelines](https://effect.website/docs/getting-started/building-pipelines/) ## Overview import { Aside } from "@astrojs/starlight/components" Effect pipelines allow for the composition and sequencing of operations on values, enabling the transformation and manipulation of data in a concise and modular manner. ## Why Pipelines are Good for Structuring Your Application Pipelines are an excellent way to structure your application and handle data transformations in a concise and modular manner. They offer several benefits: 1. **Readability**: Pipelines allow you to compose functions in a readable and sequential manner. You can clearly see the flow of data and the operations applied to it, making it easier to understand and maintain the code. 2. **Code Organization**: With pipelines, you can break down complex operations into smaller, manageable functions. Each function performs a specific task, making your code more modular and easier to reason about. 3. **Reusability**: Pipelines promote the reuse of functions. By breaking down operations into smaller functions, you can reuse them in different pipelines or contexts, improving code reuse and reducing duplication. 4. **Type Safety**: By leveraging the type system, pipelines help catch errors at compile-time. Functions in a pipeline have well-defined input and output types, ensuring that the data flows correctly through the pipeline and minimizing runtime errors. ## Functions vs Methods The use of functions in the Effect ecosystem libraries is important for achieving **tree shakeability** and ensuring **extensibility**. Functions enable efficient bundling by eliminating unused code, and they provide a flexible and modular approach to extending the libraries' functionality. ### Tree Shakeability Tree shakeability refers to the ability of a build system to eliminate unused code during the bundling process. Functions are tree shakeable, while methods are not. When functions are used in the Effect ecosystem, only the functions that are actually imported and used in your application will be included in the final bundled code. Unused functions are automatically removed, resulting in a smaller bundle size and improved performance. On the other hand, methods are attached to objects or prototypes, and they cannot be easily tree shaken. Even if you only use a subset of methods, all methods associated with an object or prototype will be included in the bundle, leading to unnecessary code bloat. ### Extensibility Another important advantage of using functions in the Effect ecosystem is the ease of extensibility. With methods, extending the functionality of an existing API often requires modifying the prototype of the object, which can be complex and error-prone. In contrast, with functions, extending the functionality is much simpler. You can define your own "extension methods" as plain old functions without the need to modify the prototypes of objects. This promotes cleaner and more modular code, and it also allows for better compatibility with other libraries and modules. ## pipe The `pipe` function is a utility that allows us to compose functions in a readable and sequential manner. It takes the output of one function and passes it as the input to the next function in the pipeline. This enables us to build complex transformations by chaining multiple functions together. **Syntax** ```ts showLineNumbers=false import { pipe } from "effect" const result = pipe(input, func1, func2, ..., funcN) ``` In this syntax, `input` is the initial value, and `func1`, `func2`, ..., `funcN` are the functions to be applied in sequence. The result of each function becomes the input for the next function, and the final result is returned. Here's an illustration of how `pipe` works: ```text showLineNumbers=false ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌────────┐ │ input │───►│ func1 │───►│ func2 │───►│ ... │───►│ funcN │───►│ result │ └───────┘ └───────┘ └───────┘ └───────┘ └───────┘ └────────┘ ``` It's important to note that functions passed to `pipe` must have a **single argument** because they are only called with a single argument. Let's see an example to better understand how `pipe` works: **Example** (Chaining Arithmetic Operations) ```ts twoslash import { pipe } from "effect" // Define simple arithmetic operations const increment = (x: number) => x + 1 const double = (x: number) => x * 2 const subtractTen = (x: number) => x - 10 // Sequentially apply these operations using `pipe` const result = pipe(5, increment, double, subtractTen) console.log(result) // Output: 2 ``` In the above example, we start with an input value of `5`. The `increment` function adds `1` to the initial value, resulting in `6`. Then, the `double` function doubles the value, giving us `12`. Finally, the `subtractTen` function subtracts `10` from `12`, resulting in the final output of `2`. The result is equivalent to `subtractTen(double(increment(5)))`, but using `pipe` makes the code more readable because the operations are sequenced from left to right, rather than nesting them inside out. ## map Transforms the value inside an effect by applying a function to it. **Syntax** ```ts showLineNumbers=false const mappedEffect = pipe(myEffect, Effect.map(transformation)) // or const mappedEffect = Effect.map(myEffect, transformation) // or const mappedEffect = myEffect.pipe(Effect.map(transformation)) ``` `Effect.map` takes a function and applies it to the value contained within an effect, creating a new effect with the transformed value. **Example** (Adding a Service Charge) Here's a practical example where we apply a service charge to a transaction amount: ```ts twoslash import { pipe, Effect } from "effect" // Function to add a small service charge to a transaction amount const addServiceCharge = (amount: number) => amount + 1 // Simulated asynchronous task to fetch a transaction amount from database const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) // Apply service charge to the transaction amount const finalAmount = pipe( fetchTransactionAmount, Effect.map(addServiceCharge) ) Effect.runPromise(finalAmount).then(console.log) // Output: 101 ``` ## as Replaces the value inside an effect with a constant value. `Effect.as` allows you to ignore the original value inside an effect and replace it with a new constant value. **Example** (Replacing a Value) ```ts twoslash import { pipe, Effect } from "effect" // Replace the value 5 with the constant "new value" const program = pipe(Effect.succeed(5), Effect.as("new value")) Effect.runPromise(program).then(console.log) // Output: "new value" ``` ## flatMap Chains effects to produce new `Effect` instances, useful for combining operations that depend on previous results. **Syntax** ```ts showLineNumbers=false const flatMappedEffect = pipe(myEffect, Effect.flatMap(transformation)) // or const flatMappedEffect = Effect.flatMap(myEffect, transformation) // or const flatMappedEffect = myEffect.pipe(Effect.flatMap(transformation)) ``` In the code above, `transformation` is the function that takes a value and returns an `Effect`, and `myEffect` is the initial `Effect` being transformed. Use `Effect.flatMap` when you need to chain multiple effects, ensuring that each step produces a new `Effect` while flattening any nested effects that may occur. It is similar to `flatMap` used with arrays but works specifically with `Effect` instances, allowing you to avoid deeply nested effect structures. **Example** (Applying a Discount) ```ts twoslash import { pipe, Effect } from "effect" // Function to apply a discount safely to a transaction amount const applyDiscount = ( total: number, discountRate: number ): Effect.Effect => discountRate === 0 ? Effect.fail(new Error("Discount rate cannot be zero")) : Effect.succeed(total - (total * discountRate) / 100) // Simulated asynchronous task to fetch a transaction amount from database const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) // Chaining the fetch and discount application using `flatMap` const finalAmount = pipe( fetchTransactionAmount, Effect.flatMap((amount) => applyDiscount(amount, 5)) ) Effect.runPromise(finalAmount).then(console.log) // Output: 95 ``` ### Ensure All Effects Are Considered Make sure that all effects within `Effect.flatMap` contribute to the final computation. If you ignore an effect, it can lead to unexpected behavior: ```ts {3} showLineNumbers=false Effect.flatMap((amount) => { // This effect will be ignored Effect.sync(() => console.log(`Apply a discount to: ${amount}`)) return applyDiscount(amount, 5) }) ``` In this case, the `Effect.sync` call is ignored and does not affect the result of `applyDiscount(amount, 5)`. To handle effects correctly, make sure to explicitly chain them using functions like `Effect.map`, `Effect.flatMap`, `Effect.andThen`, or `Effect.tap`. ## andThen Chains two actions, where the second action can depend on the result of the first. **Syntax** ```ts showLineNumbers=false const transformedEffect = pipe(myEffect, Effect.andThen(anotherEffect)) // or const transformedEffect = Effect.andThen(myEffect, anotherEffect) // or const transformedEffect = myEffect.pipe(Effect.andThen(anotherEffect)) ``` Use `andThen` when you need to run multiple actions in sequence, with the second action depending on the result of the first. This is useful for combining effects or handling computations that must happen in order. The second action can be: 1. A value (similar to `Effect.as`) 2. A function returning a value (similar to `Effect.map`) 3. A `Promise` 4. A function returning a `Promise` 5. An `Effect` 6. A function returning an `Effect` (similar to `Effect.flatMap`) **Example** (Applying a Discount Based on Fetched Amount) Let's look at an example comparing `Effect.andThen` with `Effect.map` and `Effect.flatMap`: ```ts twoslash import { pipe, Effect } from "effect" // Function to apply a discount safely to a transaction amount const applyDiscount = ( total: number, discountRate: number ): Effect.Effect => discountRate === 0 ? Effect.fail(new Error("Discount rate cannot be zero")) : Effect.succeed(total - (total * discountRate) / 100) // Simulated asynchronous task to fetch a transaction amount from database const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) // Using Effect.map and Effect.flatMap const result1 = pipe( fetchTransactionAmount, Effect.map((amount) => amount * 2), Effect.flatMap((amount) => applyDiscount(amount, 5)) ) Effect.runPromise(result1).then(console.log) // Output: 190 // Using Effect.andThen const result2 = pipe( fetchTransactionAmount, Effect.andThen((amount) => amount * 2), Effect.andThen((amount) => applyDiscount(amount, 5)) ) Effect.runPromise(result2).then(console.log) // Output: 190 ``` ### Option and Either with andThen Both [Option](/docs/data-types/option/#interop-with-effect) and [Either](/docs/data-types/either/#interop-with-effect) are commonly used for handling optional or missing values or simple error cases. These types integrate well with `Effect.andThen`. When used with `Effect.andThen`, the operations are categorized as scenarios 5 and 6 (as discussed earlier) because both `Option` and `Either` are treated as effects in this context. **Example** (with Option) ```ts twoslash import { pipe, Effect, Option } from "effect" // Simulated asynchronous task fetching a number from a database const fetchNumberValue = Effect.tryPromise(() => Promise.resolve(42)) // ┌─── Effect // ▼ const program = pipe( fetchNumberValue, Effect.andThen((x) => (x > 0 ? Option.some(x) : Option.none())) ) ``` You might expect the type of `program` to be `Effect, UnknownException, never>`, but it is actually `Effect`. This is because `Option` is treated as an effect of type `Effect`, and as a result, the possible errors are combined into a union type. **Example** (with Either) ```ts twoslash import { pipe, Effect, Either } from "effect" // Function to parse an integer from a string that can fail const parseInteger = (input: string): Either.Either => isNaN(parseInt(input)) ? Either.left("Invalid integer") : Either.right(parseInt(input)) // Simulated asynchronous task fetching a string from database const fetchStringValue = Effect.tryPromise(() => Promise.resolve("42")) // ┌─── Effect // ▼ const program = pipe( fetchStringValue, Effect.andThen((str) => parseInteger(str)) ) ``` Although one might expect the type of `program` to be `Effect, UnknownException, never>`, it is actually `Effect`. This is because `Either` is treated as an effect of type `Effect`, meaning the errors are combined into a union type. ## tap Runs a side effect with the result of an effect without changing the original value. Use `Effect.tap` when you want to perform a side effect, like logging or tracking, without modifying the main value. This is useful when you need to observe or record an action but want the original value to be passed to the next step. `Effect.tap` works similarly to `Effect.flatMap`, but it ignores the result of the function passed to it. The value from the previous effect remains available for the next part of the chain. Note that if the side effect fails, the entire chain will fail too. **Example** (Logging a step in a pipeline) ```ts twoslash import { pipe, Effect, Console } from "effect" // Function to apply a discount safely to a transaction amount const applyDiscount = ( total: number, discountRate: number ): Effect.Effect => discountRate === 0 ? Effect.fail(new Error("Discount rate cannot be zero")) : Effect.succeed(total - (total * discountRate) / 100) // Simulated asynchronous task to fetch a transaction amount from database const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) const finalAmount = pipe( fetchTransactionAmount, // Log the fetched transaction amount Effect.tap((amount) => Console.log(`Apply a discount to: ${amount}`)), // `amount` is still available! Effect.flatMap((amount) => applyDiscount(amount, 5)) ) Effect.runPromise(finalAmount).then(console.log) /* Output: Apply a discount to: 100 95 */ ``` In this example, `Effect.tap` is used to log the transaction amount before applying the discount, without modifying the value itself. The original value (`amount`) remains available for the next operation (`applyDiscount`). Using `Effect.tap` allows us to execute side effects during the computation without altering the result. This can be useful for logging, performing additional actions, or observing the intermediate values without interfering with the main computation flow. ## all Combines multiple effects into one, returning results based on the input structure. Use `Effect.all` when you need to run multiple effects and combine their results into a single output. It supports tuples, iterables, structs, and records, making it flexible for different input types. For instance, if the input is a tuple: ```ts showLineNumbers=false // ┌─── a tuple of effects // ▼ Effect.all([effect1, effect2, ...]) ``` the effects are executed in order, and the result is a new effect containing the results as a tuple. The results in the tuple match the order of the effects passed to `Effect.all`. By default, `Effect.all` runs effects sequentially and produces a tuple or object with the results. If any effect fails, it stops execution (short-circuiting) and propagates the error. See [Collecting](/docs/getting-started/control-flow/#all) for more information on how to use `Effect.all`. **Example** (Combining Configuration and Database Checks) ```ts twoslash import { Effect } from "effect" // Simulated function to read configuration from a file const webConfig = Effect.promise(() => Promise.resolve({ dbConnection: "localhost", port: 8080 }) ) // Simulated function to test database connectivity const checkDatabaseConnectivity = Effect.promise(() => Promise.resolve("Connected to Database") ) // Combine both effects to perform startup checks const startupChecks = Effect.all([webConfig, checkDatabaseConnectivity]) Effect.runPromise(startupChecks).then(([config, dbStatus]) => { console.log( `Configuration: ${JSON.stringify(config)}\nDB Status: ${dbStatus}` ) }) /* Output: Configuration: {"dbConnection":"localhost","port":8080} DB Status: Connected to Database */ ``` ## Build your first pipeline Let's now combine the `pipe` function, `Effect.all`, and `Effect.andThen` to create a pipeline that performs a sequence of transformations. **Example** (Building a Transaction Pipeline) ```ts twoslash import { Effect, pipe } from "effect" // Function to add a small service charge to a transaction amount const addServiceCharge = (amount: number) => amount + 1 // Function to apply a discount safely to a transaction amount const applyDiscount = ( total: number, discountRate: number ): Effect.Effect => discountRate === 0 ? Effect.fail(new Error("Discount rate cannot be zero")) : Effect.succeed(total - (total * discountRate) / 100) // Simulated asynchronous task to fetch a transaction amount from database const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) // Simulated asynchronous task to fetch a discount rate // from a configuration file const fetchDiscountRate = Effect.promise(() => Promise.resolve(5)) // Assembling the program using a pipeline of effects const program = pipe( // Combine both fetch effects to get the transaction amount // and discount rate Effect.all([fetchTransactionAmount, fetchDiscountRate]), // Apply the discount to the transaction amount Effect.andThen(([transactionAmount, discountRate]) => applyDiscount(transactionAmount, discountRate) ), // Add the service charge to the discounted amount Effect.andThen(addServiceCharge), // Format the final result for display Effect.andThen( (finalAmount) => `Final amount to charge: ${finalAmount}` ) ) // Execute the program and log the result Effect.runPromise(program).then(console.log) // Output: "Final amount to charge: 96" ``` This pipeline demonstrates how you can structure your code by combining different effects into a clear, readable flow. ## The pipe method Effect provides a `pipe` method that works similarly to the `pipe` method found in [rxjs](https://rxjs.dev/api/index/function/pipe). This method allows you to chain multiple operations together, making your code more concise and readable. **Syntax** ```ts showLineNumbers=false const result = effect.pipe(func1, func2, ..., funcN) ``` This is equivalent to using the `pipe` **function** like this: ```ts showLineNumbers=false const result = pipe(effect, func1, func2, ..., funcN) ``` The `pipe` method is available on all effects and many other data types, eliminating the need to import the `pipe` function and saving you some keystrokes. **Example** (Using the `pipe` Method) Let's rewrite an [earlier example](#build-your-first-pipeline), this time using the `pipe` method. ```ts twoslash collapse={3-15} import { Effect } from "effect" const addServiceCharge = (amount: number) => amount + 1 const applyDiscount = ( total: number, discountRate: number ): Effect.Effect => discountRate === 0 ? Effect.fail(new Error("Discount rate cannot be zero")) : Effect.succeed(total - (total * discountRate) / 100) const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) const fetchDiscountRate = Effect.promise(() => Promise.resolve(5)) const program = Effect.all([ fetchTransactionAmount, fetchDiscountRate ]).pipe( Effect.andThen(([transactionAmount, discountRate]) => applyDiscount(transactionAmount, discountRate) ), Effect.andThen(addServiceCharge), Effect.andThen( (finalAmount) => `Final amount to charge: ${finalAmount}` ) ) ``` ## Cheatsheet Let's summarize the transformation functions we have seen so far: | API | Input | Output | | --------- | ----------------------------------------- | --------------------------- | | `map` | `Effect`, `A => B` | `Effect` | | `flatMap` | `Effect`, `A => Effect` | `Effect` | | `andThen` | `Effect`, \* | `Effect` | | `tap` | `Effect`, `A => Effect` | `Effect` | | `all` | `[Effect, Effect, ...]` | `Effect<[A, B, ...], E, R>` | # [Control Flow Operators](https://effect.website/docs/getting-started/control-flow/) ## Overview Even though JavaScript provides built-in control flow structures, Effect offers additional control flow functions that are useful in Effect applications. In this section, we will introduce different ways to control the flow of execution. ## if Expression When working with Effect values, we can use standard JavaScript if-then-else statements: **Example** (Returning None for Invalid Weight) Here we are using the [Option](/docs/data-types/option/) data type to represent the absence of a valid value. ```ts twoslash import { Effect, Option } from "effect" // Function to validate weight and return an Option const validateWeightOption = ( weight: number ): Effect.Effect> => { if (weight >= 0) { // Return Some if the weight is valid return Effect.succeed(Option.some(weight)) } else { // Return None if the weight is invalid return Effect.succeed(Option.none()) } } ``` **Example** (Returning Error for Invalid Weight) You can also handle invalid inputs by using the error channel, which allows you to return an error when the input is invalid: ```ts twoslash import { Effect } from "effect" // Function to validate weight or fail with an error const validateWeightOrFail = ( weight: number ): Effect.Effect => { if (weight >= 0) { // Return the weight if valid return Effect.succeed(weight) } else { // Fail with an error if invalid return Effect.fail(`negative input: ${weight}`) } } ``` ## Conditional Operators ### if Executes one of two effects based on a condition evaluated by an effectful predicate. Use `Effect.if` to run one of two effects depending on whether the predicate effect evaluates to `true` or `false`. If the predicate is `true`, the `onTrue` effect is executed. If it is `false`, the `onFalse` effect is executed instead. **Example** (Simulating a Coin Flip) In this example, we simulate a virtual coin flip using `Random.nextBoolean` to generate a random boolean value. If the value is `true`, the `onTrue` effect logs "Head". If the value is `false`, the `onFalse` effect logs "Tail". ```ts twoslash import { Effect, Random, Console } from "effect" const flipTheCoin = Effect.if(Random.nextBoolean, { onTrue: () => Console.log("Head"), // Runs if the predicate is true onFalse: () => Console.log("Tail") // Runs if the predicate is false }) Effect.runFork(flipTheCoin) ``` ### when Conditionally executes an effect based on a boolean condition. `Effect.when` allows you to conditionally execute an effect, similar to using an `if (condition)` expression, but with the added benefit of handling effects. If the condition is `true`, the effect is executed; otherwise, it does nothing. The result of the effect is wrapped in an `Option` to indicate whether the effect was executed. If the condition is `true`, the result of the effect is wrapped in a `Some`. If the condition is `false`, the result is `None`, representing that the effect was skipped. **Example** (Conditional Effect Execution) ```ts twoslash import { Effect, Option } from "effect" const validateWeightOption = ( weight: number ): Effect.Effect> => // Conditionally execute the effect if the weight is non-negative Effect.succeed(weight).pipe(Effect.when(() => weight >= 0)) // Run with a valid weight Effect.runPromise(validateWeightOption(100)).then(console.log) /* Output: { _id: "Option", _tag: "Some", value: 100 } */ // Run with an invalid weight Effect.runPromise(validateWeightOption(-5)).then(console.log) /* Output: { _id: "Option", _tag: "None" } */ ``` In this example, the [Option](/docs/data-types/option/) data type is used to represent the presence or absence of a valid value. If the condition evaluates to `true` (in this case, if the weight is non-negative), the effect is executed and wrapped in a `Some`. Otherwise, the result is `None`. ### whenEffect Executes an effect conditionally, based on the result of another effect. Use `Effect.whenEffect` when the condition to determine whether to execute the effect depends on the outcome of another effect that produces a boolean value. If the condition effect evaluates to `true`, the specified effect is executed. If it evaluates to `false`, no effect is executed. The result of the effect is wrapped in an `Option` to indicate whether the effect was executed. If the condition is `true`, the result of the effect is wrapped in a `Some`. If the condition is `false`, the result is `None`, representing that the effect was skipped. **Example** (Using an Effect as a Condition) The following function creates a random integer, but only if a randomly generated boolean is `true`. ```ts twoslash import { Effect, Random } from "effect" const randomIntOption = Random.nextInt.pipe( Effect.whenEffect(Random.nextBoolean) ) console.log(Effect.runSync(randomIntOption)) /* Example Output: { _id: 'Option', _tag: 'Some', value: 8609104974198840 } */ ``` ### unless / unlessEffect The `Effect.unless` and `Effect.unlessEffect` functions are similar to the `when*` functions, but they are equivalent to the `if (!condition) expression` construct. ## Zipping ### zip Combines two effects into a single effect, producing a tuple with the results of both effects. The `Effect.zip` function executes the first effect (left) and then the second effect (right). Once both effects succeed, their results are combined into a tuple. **Example** (Combining Two Effects Sequentially) ```ts twoslash import { Effect } from "effect" const task1 = Effect.succeed(1).pipe( Effect.delay("200 millis"), Effect.tap(Effect.log("task1 done")) ) const task2 = Effect.succeed("hello").pipe( Effect.delay("100 millis"), Effect.tap(Effect.log("task2 done")) ) // Combine the two effects together // // ┌─── Effect<[number, string], never, never> // ▼ const program = Effect.zip(task1, task2) Effect.runPromise(program).then(console.log) /* Output: timestamp=... level=INFO fiber=#0 message="task1 done" timestamp=... level=INFO fiber=#0 message="task2 done" [ 1, 'hello' ] */ ``` By default, the effects are run sequentially. To run them concurrently, use the `{ concurrent: true }` option. **Example** (Combining Two Effects Concurrently) ```ts collapse={3-11} "{ concurrent: true }" "task2 done" import { Effect } from "effect" const task1 = Effect.succeed(1).pipe( Effect.delay("200 millis"), Effect.tap(Effect.log("task1 done")) ) const task2 = Effect.succeed("hello").pipe( Effect.delay("100 millis"), Effect.tap(Effect.log("task2 done")) ) // Run both effects concurrently using the concurrent option const program = Effect.zip(task1, task2, { concurrent: true }) Effect.runPromise(program).then(console.log) /* Output: timestamp=... level=INFO fiber=#3 message="task2 done" timestamp=... level=INFO fiber=#2 message="task1 done" [ 1, 'hello' ] */ ``` In this concurrent version, both effects run in parallel. `task2` completes first, but both tasks can be logged and processed as soon as they're done. ### zipWith Combines two effects sequentially and applies a function to their results to produce a single value. The `Effect.zipWith` function is similar to [Effect.zip](#zip), but instead of returning a tuple of results, it applies a provided function to the results of the two effects, combining them into a single value. By default, the effects are run sequentially. To run them concurrently, use the `{ concurrent: true }` option. **Example** (Combining Effects with a Custom Function) ```ts twoslash import { Effect } from "effect" const task1 = Effect.succeed(1).pipe( Effect.delay("200 millis"), Effect.tap(Effect.log("task1 done")) ) const task2 = Effect.succeed("hello").pipe( Effect.delay("100 millis"), Effect.tap(Effect.log("task2 done")) ) // ┌─── Effect // ▼ const task3 = Effect.zipWith( task1, task2, // Combines results into a single value (number, string) => number + string.length ) Effect.runPromise(task3).then(console.log) /* Output: timestamp=... level=INFO fiber=#3 message="task1 done" timestamp=... level=INFO fiber=#2 message="task2 done" 6 */ ``` ## Looping ### loop The `Effect.loop` function allows you to repeatedly update a state using a `step` function until a condition defined by the `while` function becomes `false`. It collects the intermediate states in an array and returns them as the final result. **Syntax** ```ts showLineNumbers=false Effect.loop(initial, { while: (state) => boolean, step: (state) => state, body: (state) => Effect }) ``` This function is similar to a `while` loop in JavaScript, with the addition of effectful computations: ```ts showLineNumbers=false let state = initial const result = [] while (options.while(state)) { result.push(options.body(state)) // Perform the effectful operation state = options.step(state) // Update the state } return result ``` **Example** (Looping with Collected Results) ```ts twoslash import { Effect } from "effect" // A loop that runs 5 times, collecting each iteration's result const result = Effect.loop( // Initial state 1, { // Condition to continue looping while: (state) => state <= 5, // State update function step: (state) => state + 1, // Effect to be performed on each iteration body: (state) => Effect.succeed(state) } ) Effect.runPromise(result).then(console.log) // Output: [1, 2, 3, 4, 5] ``` In this example, the loop starts with the state `1` and continues until the state exceeds `5`. Each state is incremented by `1` and is collected into an array, which becomes the final result. #### Discarding Intermediate Results The `discard` option, when set to `true`, will discard the results of each effectful operation, returning `void` instead of an array. **Example** (Loop with Discarded Results) ```ts twoslash "discard: true" import { Effect, Console } from "effect" const result = Effect.loop( // Initial state 1, { // Condition to continue looping while: (state) => state <= 5, // State update function step: (state) => state + 1, // Effect to be performed on each iteration body: (state) => Console.log(`Currently at state ${state}`), // Discard intermediate results discard: true } ) Effect.runPromise(result).then(console.log) /* Output: Currently at state 1 Currently at state 2 Currently at state 3 Currently at state 4 Currently at state 5 undefined */ ``` In this example, the loop performs a side effect of logging the current index on each iteration, but it discards all intermediate results. The final result is `undefined`. ### iterate The `Effect.iterate` function lets you repeatedly update a state through an effectful operation. It runs the `body` effect to update the state in each iteration and continues as long as the `while` condition evaluates to `true`. **Syntax** ```ts showLineNumbers=false Effect.iterate(initial, { while: (result) => boolean, body: (result) => Effect }) ``` This function is similar to a `while` loop in JavaScript, with the addition of effectful computations: ```ts showLineNumbers=false let result = initial while (options.while(result)) { result = options.body(result) } return result ``` **Example** (Effectful Iteration) ```ts twoslash import { Effect } from "effect" const result = Effect.iterate( // Initial result 1, { // Condition to continue iterating while: (result) => result <= 5, // Operation to change the result body: (result) => Effect.succeed(result + 1) } ) Effect.runPromise(result).then(console.log) // Output: 6 ``` ### forEach Executes an effectful operation for each element in an `Iterable`. The `Effect.forEach` function applies a provided operation to each element in the iterable, producing a new effect that returns an array of results. If any effect fails, the iteration stops immediately (short-circuiting), and the error is propagated. The `concurrency` option controls how many operations are performed concurrently. By default, the operations are performed sequentially. **Example** (Applying Effects to Iterable Elements) ```ts twoslash import { Effect, Console } from "effect" const result = Effect.forEach([1, 2, 3, 4, 5], (n, index) => Console.log(`Currently at index ${index}`).pipe(Effect.as(n * 2)) ) Effect.runPromise(result).then(console.log) /* Output: Currently at index 0 Currently at index 1 Currently at index 2 Currently at index 3 Currently at index 4 [ 2, 4, 6, 8, 10 ] */ ``` In this example, we iterate over the array `[1, 2, 3, 4, 5]`, applying an effect that logs the current index. The `Effect.as(n * 2)` operation transforms each value, resulting in an array `[2, 4, 6, 8, 10]`. The final output is the result of collecting all the transformed values. #### Discarding Results The `discard` option, when set to `true`, will discard the results of each effectful operation, returning `void` instead of an array. **Example** (Using `discard` to Ignore Results) ```ts twoslash "{ discard: true }" import { Effect, Console } from "effect" // Apply effects but discard the results const result = Effect.forEach( [1, 2, 3, 4, 5], (n, index) => Console.log(`Currently at index ${index}`).pipe(Effect.as(n * 2)), { discard: true } ) Effect.runPromise(result).then(console.log) /* Output: Currently at index 0 Currently at index 1 Currently at index 2 Currently at index 3 Currently at index 4 undefined */ ``` In this case, the effects still run for each element, but the results are discarded, so the final output is `undefined`. ## Collecting ### all Combines multiple effects into one, returning results based on the input structure. Use `Effect.all` when you need to run multiple effects and combine their results into a single output. It supports tuples, iterables, structs, and records, making it flexible for different input types. If any effect fails, it stops execution (short-circuiting) and propagates the error. To change this behavior, you can use the [`mode`](#the-mode-option) option, which allows all effects to run and collect results as [Either](/docs/data-types/either/) or [Option](/docs/data-types/option/). You can control the execution order (e.g., sequential vs. concurrent) using the [Concurrency Options](/docs/concurrency/basic-concurrency/#concurrency-options). For instance, if the input is a tuple: ```ts showLineNumbers=false // ┌─── a tuple of effects // ▼ Effect.all([effect1, effect2, ...]) ``` the effects are executed sequentially, and the result is a new effect containing the results as a tuple. The results in the tuple match the order of the effects passed to `Effect.all`. Let's explore examples for different types of structures: tuples, iterables, objects, and records. **Example** (Combining Effects in Tuples) ```ts twoslash import { Effect, Console } from "effect" const tupleOfEffects = [ Effect.succeed(42).pipe(Effect.tap(Console.log)), Effect.succeed("Hello").pipe(Effect.tap(Console.log)) ] as const // ┌─── Effect<[number, string], never, never> // ▼ const resultsAsTuple = Effect.all(tupleOfEffects) Effect.runPromise(resultsAsTuple).then(console.log) /* Output: 42 Hello [ 42, 'Hello' ] */ ``` **Example** (Combining Effects in Iterables) ```ts twoslash import { Effect, Console } from "effect" const iterableOfEffects: Iterable> = [1, 2, 3].map( (n) => Effect.succeed(n).pipe(Effect.tap(Console.log)) ) // ┌─── Effect // ▼ const resultsAsArray = Effect.all(iterableOfEffects) Effect.runPromise(resultsAsArray).then(console.log) /* Output: 1 2 3 [ 1, 2, 3 ] */ ``` **Example** (Combining Effects in Structs) ```ts twoslash import { Effect, Console } from "effect" const structOfEffects = { a: Effect.succeed(42).pipe(Effect.tap(Console.log)), b: Effect.succeed("Hello").pipe(Effect.tap(Console.log)) } // ┌─── Effect<{ a: number; b: string; }, never, never> // ▼ const resultsAsStruct = Effect.all(structOfEffects) Effect.runPromise(resultsAsStruct).then(console.log) /* Output: 42 Hello { a: 42, b: 'Hello' } */ ``` **Example** (Combining Effects in Records) ```ts twoslash import { Effect, Console } from "effect" const recordOfEffects: Record> = { key1: Effect.succeed(1).pipe(Effect.tap(Console.log)), key2: Effect.succeed(2).pipe(Effect.tap(Console.log)) } // ┌─── Effect<{ [x: string]: number; }, never, never> // ▼ const resultsAsRecord = Effect.all(recordOfEffects) Effect.runPromise(resultsAsRecord).then(console.log) /* Output: 1 2 { key1: 1, key2: 2 } */ ``` #### Short-Circuiting Behavior The `Effect.all` function stops execution on the first error it encounters, this is called "short-circuiting". If any effect in the collection fails, the remaining effects will not run, and the error will be propagated. **Example** (Bail Out on First Failure) ```ts twoslash import { Effect, Console } from "effect" const program = Effect.all([ Effect.succeed("Task1").pipe(Effect.tap(Console.log)), Effect.fail("Task2: Oh no!").pipe(Effect.tap(Console.log)), // Won't execute due to earlier failure Effect.succeed("Task3").pipe(Effect.tap(Console.log)) ]) Effect.runPromiseExit(program).then(console.log) /* Output: Task1 { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Task2: Oh no!' } } */ ``` You can override this behavior by using the `mode` option. #### The `mode` option The `{ mode: "either" }` option changes the behavior of `Effect.all` to ensure all effects run, even if some fail. Instead of stopping on the first failure, this mode collects both successes and failures, returning an array of `Either` instances where each result is either a `Right` (success) or a `Left` (failure). **Example** (Collecting Results with `mode: "either"`) ```ts twoslash /{ mode: "either" }/ import { Effect, Console } from "effect" const effects = [ Effect.succeed("Task1").pipe(Effect.tap(Console.log)), Effect.fail("Task2: Oh no!").pipe(Effect.tap(Console.log)), Effect.succeed("Task3").pipe(Effect.tap(Console.log)) ] const program = Effect.all(effects, { mode: "either" }) Effect.runPromiseExit(program).then(console.log) /* Output: Task1 Task3 { _id: 'Exit', _tag: 'Success', value: [ { _id: 'Either', _tag: 'Right', right: 'Task1' }, { _id: 'Either', _tag: 'Left', left: 'Task2: Oh no!' }, { _id: 'Either', _tag: 'Right', right: 'Task3' } ] } */ ``` Similarly, the `{ mode: "validate" }` option uses `Option` to indicate success or failure. Each effect returns `None` for success and `Some` with the error for failure. **Example** (Collecting Results with `mode: "validate"`) ```ts twoslash /{ mode: "validate" }/ import { Effect, Console } from "effect" const effects = [ Effect.succeed("Task1").pipe(Effect.tap(Console.log)), Effect.fail("Task2: Oh no!").pipe(Effect.tap(Console.log)), Effect.succeed("Task3").pipe(Effect.tap(Console.log)) ] const program = Effect.all(effects, { mode: "validate" }) Effect.runPromiseExit(program).then((result) => console.log("%o", result)) /* Output: Task1 Task3 { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: [ { _id: 'Option', _tag: 'None' }, { _id: 'Option', _tag: 'Some', value: 'Task2: Oh no!' }, { _id: 'Option', _tag: 'None' } ] } } */ ``` # [Create Effect App](https://effect.website/docs/getting-started/create-effect-app/) ## Overview import { Tabs, TabItem } from "@astrojs/starlight/components" The `create-effect-app` CLI allow you to create a new Effect application using a default template or an [example](https://github.com/Effect-TS/examples) from a public Github repository. It is the easiest way to get started with Effect. ## CLI To begin, run the `create-effect-app` command in your terminal using your preferred package manager: ```sh showLineNumbers=false npx create-effect-app@latest ``` ```sh showLineNumbers=false pnpm create effect-app@latest ``` ```sh showLineNumbers=false yarn create effect-app@latest ``` ```sh showLineNumbers=false bunx create-effect-app@latest ``` ```sh showLineNumbers=false deno init --npm effect-app@latest ``` This command starts an interactive setup that guides you through the steps required to bootstrap your project: ![create-effect-app](../_assets/create-effect-app.gif "Animated GIF demonstrating the interactive experience when create-effect-app is run in interactive mode") After making your selections, `create-effect-app` will generate your new Effect project and configure it based on your choices. **Example** For instance, to create a new Effect project in a directory named `"my-effect-app"` using the basic template with ESLint integration, you can run: ```sh showLineNumbers=false npx create-effect-app --template basic --eslint my-effect-app ``` ## Non-Interactive Usage If you prefer, `create-effect-app` can also be used in a non-interactive mode: ```sh showLineNumbers=false create-effect-app (-t, --template basic | cli | monorepo) [--changesets] [--flake] [--eslint] [--workflows] [] create-effect-app (-e, --example http-server) [] ``` Below is a breakdown of the available options to customize an Effect project template: | Option | Description | | -------------- | ---------------------------------------------------------------------------------- | | `--changesets` | Initializes your project with the Changesets package for managing version control. | | `--flake` | Initializes your project with a Nix flake for managing system dependencies. | | `--eslint` | Includes ESLint for code formatting and linting. | | `--workflows` | Sets up Effect's recommended GitHub Action workflows for automation. | # [Creating Effects](https://effect.website/docs/getting-started/creating-effects/) ## Overview import { Aside } from "@astrojs/starlight/components" Effect provides different ways to create effects, which are units of computation that encapsulate side effects. In this guide, we will cover some of the common methods that you can use to create effects. ## Why Not Throw Errors? In traditional programming, when an error occurs, it is often handled by throwing an exception: ```ts twoslash // Type signature doesn't show possible exceptions const divide = (a: number, b: number): number => { if (b === 0) { throw new Error("Cannot divide by zero") } return a / b } ``` However, throwing errors can be problematic. The type signatures of functions do not indicate that they can throw exceptions, making it difficult to reason about potential errors. To address this issue, Effect introduces dedicated constructors for creating effects that represent both success and failure: `Effect.succeed` and `Effect.fail`. These constructors allow you to explicitly handle success and failure cases while **leveraging the type system to track errors**. ### succeed Creates an `Effect` that always succeeds with a given value. Use this function when you need an effect that completes successfully with a specific value without any errors or external dependencies. **Example** (Creating a Successful Effect) ```ts twoslash import { Effect } from "effect" // ┌─── Effect // ▼ const success = Effect.succeed(42) ``` The type of `success` is `Effect`, which means: - It produces a value of type `number`. - It does not generate any errors (`never` indicates no errors). - It requires no additional data or dependencies (`never` indicates no requirements). ```text showLineNumbers=false ┌─── Produces a value of type number │ ┌─── Does not generate any errors │ │ ┌─── Requires no dependencies ▼ ▼ ▼ Effect ``` ### fail Creates an `Effect` that represents an error that can be recovered from. Use this function to explicitly signal an error in an `Effect`. The error will keep propagating unless it is handled. You can handle the error with functions like [Effect.catchAll](/docs/error-management/expected-errors/#catchall) or [Effect.catchTag](/docs/error-management/expected-errors/#catchtag). **Example** (Creating a Failed Effect) ```ts twoslash import { Effect } from "effect" // ┌─── Effect // ▼ const failure = Effect.fail( new Error("Operation failed due to network error") ) ``` The type of `failure` is `Effect`, which means: - It never produces a value (`never` indicates that no successful result will be produced). - It fails with an error, specifically an `Error`. - It requires no additional data or dependencies (`never` indicates no requirements). ```text showLineNumbers=false ┌─── Never produces a value │ ┌─── Fails with an Error │ │ ┌─── Requires no dependencies ▼ ▼ ▼ Effect ``` Although you can use `Error` objects with `Effect.fail`, you can also pass strings, numbers, or more complex objects depending on your error management strategy. Using "tagged" errors (objects with a `_tag` field) can help identify error types and works well with standard Effect functions, like [Effect.catchTag](/docs/error-management/expected-errors/#catchtag). **Example** (Using Tagged Errors) ```ts twoslash import { Effect } from "effect" class HttpError { readonly _tag = "HttpError" } // ┌─── Effect // ▼ const program = Effect.fail(new HttpError()) ``` ## Error Tracking With `Effect.succeed` and `Effect.fail`, you can explicitly handle success and failure cases and the type system will ensure that errors are tracked and accounted for. **Example** (Rewriting a Division Function) Here's how you can rewrite the [`divide`](#why-not-throw-errors) function using Effect, making error handling explicit. ```ts twoslash import { Effect } from "effect" const divide = (a: number, b: number): Effect.Effect => b === 0 ? Effect.fail(new Error("Cannot divide by zero")) : Effect.succeed(a / b) ``` In this example, the `divide` function indicates in its return type `Effect` that the operation can either succeed with a `number` or fail with an `Error`. ```text showLineNumbers=false ┌─── Produces a value of type number │ ┌─── Fails with an Error ▼ ▼ Effect ``` This clear type signature helps ensure that errors are handled properly and that anyone calling the function is aware of the possible outcomes. **Example** (Simulating a User Retrieval Operation) Let's imagine another scenario where we use `Effect.succeed` and `Effect.fail` to model a simple user retrieval operation where the user data is hardcoded, which could be useful in testing scenarios or when mocking data: ```ts twoslash import { Effect } from "effect" // Define a User type interface User { readonly id: number readonly name: string } // A mocked function to simulate fetching a user from a database const getUser = (userId: number): Effect.Effect => { // Normally, you would access a database or API here, but we'll mock it const userDatabase: Record = { 1: { id: 1, name: "John Doe" }, 2: { id: 2, name: "Jane Smith" } } // Check if the user exists in our "database" and return appropriately const user = userDatabase[userId] if (user) { return Effect.succeed(user) } else { return Effect.fail(new Error("User not found")) } } // When executed, this will successfully return the user with id 1 const exampleUserEffect = getUser(1) ``` In this example, `exampleUserEffect`, which has the type `Effect`, will either produce a `User` object or an `Error`, depending on whether the user exists in the mocked database. For a deeper dive into managing errors in your applications, refer to the [Error Management Guide](/docs/error-management/expected-errors/). ## Modeling Synchronous Effects In JavaScript, you can delay the execution of synchronous computations using "thunks". Thunks are useful for delaying the computation of a value until it is needed. To model synchronous side effects, Effect provides the `Effect.sync` and `Effect.try` constructors, which accept a thunk. ### sync Creates an `Effect` that represents a synchronous side-effectful computation. Use `Effect.sync` when you are sure the operation will not fail. The provided function (`thunk`) must not throw errors; if it does, the error will be treated as a ["defect"](/docs/error-management/unexpected-errors/). This defect is not a standard error but indicates a flaw in the logic that was expected to be error-free. You can think of it similar to an unexpected crash in the program, which can be further managed or logged using tools like [Effect.catchAllDefect](/docs/error-management/unexpected-errors/#catchalldefect). This feature ensures that even unexpected failures in your application are not lost and can be handled appropriately. **Example** (Logging a Message) In the example below, `Effect.sync` is used to defer the side-effect of writing to the console. ```ts twoslash import { Effect } from "effect" const log = (message: string) => Effect.sync(() => { console.log(message) // side effect }) // ┌─── Effect // ▼ const program = log("Hello, World!") ``` The side effect (logging to the console) encapsulated within `program` won't occur until the effect is explicitly run (see the [Running Effects](/docs/getting-started/running-effects/) section for more details). This allows you to define side effects at one point in your code and control when they are activated, improving manageability and predictability of side effects in larger applications. ### try Creates an `Effect` that represents a synchronous computation that might fail. In situations where you need to perform synchronous operations that might fail, such as parsing JSON, you can use the `Effect.try` constructor. This constructor is designed to handle operations that could throw exceptions by capturing those exceptions and transforming them into manageable errors. **Example** (Safe JSON Parsing) Suppose you have a function that attempts to parse a JSON string. This operation can fail and throw an error if the input string is not properly formatted as JSON: ```ts twoslash import { Effect } from "effect" const parse = (input: string) => // This might throw an error if input is not valid JSON Effect.try(() => JSON.parse(input)) // ┌─── Effect // ▼ const program = parse("") ``` In this example: - `parse` is a function that creates an effect encapsulating the JSON parsing operation. - If `JSON.parse(input)` throws an error due to invalid input, `Effect.try` catches this error and the effect represented by `program` will fail with an `UnknownException`. This ensures that errors are not silently ignored but are instead handled within the structured flow of effects. #### Customizing Error Handling You might want to transform the caught exception into a more specific error or perform additional operations when catching an error. `Effect.try` supports an overload that allows you to specify how caught exceptions should be transformed: **Example** (Custom Error Handling) ```ts twoslash {8} import { Effect } from "effect" const parse = (input: string) => Effect.try({ // JSON.parse may throw for bad input try: () => JSON.parse(input), // remap the error catch: (unknown) => new Error(`something went wrong ${unknown}`) }) // ┌─── Effect // ▼ const program = parse("") ``` You can think of this as a similar pattern to the traditional try-catch block in JavaScript: ```ts showLineNumbers=false try { return JSON.parse(input) } catch (unknown) { throw new Error(`something went wrong ${unknown}`) } ``` ## Modeling Asynchronous Effects In traditional programming, we often use `Promise`s to handle asynchronous computations. However, dealing with errors in promises can be problematic. By default, `Promise` only provides the type `Value` for the resolved value, which means errors are not reflected in the type system. This limits the expressiveness and makes it challenging to handle and track errors effectively. To overcome these limitations, Effect introduces dedicated constructors for creating effects that represent both success and failure in an asynchronous context: `Effect.promise` and `Effect.tryPromise`. These constructors allow you to explicitly handle success and failure cases while **leveraging the type system to track errors**. ### promise Creates an `Effect` that represents an asynchronous computation guaranteed to succeed. Use `Effect.promise` when you are sure the operation will not reject. The provided function (`thunk`) returns a `Promise` that should never reject; if it does, the error will be treated as a ["defect"](/docs/error-management/unexpected-errors/). This defect is not a standard error but indicates a flaw in the logic that was expected to be error-free. You can think of it similar to an unexpected crash in the program, which can be further managed or logged using tools like [Effect.catchAllDefect](/docs/error-management/unexpected-errors/#catchalldefect). This feature ensures that even unexpected failures in your application are not lost and can be handled appropriately. **Example** (Delayed Message) ```ts twoslash import { Effect } from "effect" const delay = (message: string) => Effect.promise( () => new Promise((resolve) => { setTimeout(() => { resolve(message) }, 2000) }) ) // ┌─── Effect // ▼ const program = delay("Async operation completed successfully!") ``` The `program` value has the type `Effect` and can be interpreted as an effect that: - succeeds with a value of type `string` - does not produce any expected error (`never`) - does not require any context (`never`) ### tryPromise Creates an `Effect` that represents an asynchronous computation that might fail. Unlike `Effect.promise`, this constructor is suitable when the underlying `Promise` might reject. It provides a way to catch errors and handle them appropriately. By default if an error occurs, it will be caught and propagated to the error channel as as an `UnknownException`. **Example** (Fetching a TODO Item) ```ts twoslash import { Effect } from "effect" const getTodo = (id: number) => // Will catch any errors and propagate them as UnknownException Effect.tryPromise(() => fetch(`https://jsonplaceholder.typicode.com/todos/${id}`) ) // ┌─── Effect // ▼ const program = getTodo(1) ``` The `program` value has the type `Effect` and can be interpreted as an effect that: - succeeds with a value of type `Response` - might produce an error (`UnknownException`) - does not require any context (`never`) #### Customizing Error Handling If you want more control over what gets propagated to the error channel, you can use an overload of `Effect.tryPromise` that takes a remapping function: **Example** (Custom Error Handling) ```ts twoslash {7} import { Effect } from "effect" const getTodo = (id: number) => Effect.tryPromise({ try: () => fetch(`https://jsonplaceholder.typicode.com/todos/${id}`), // remap the error catch: (unknown) => new Error(`something went wrong ${unknown}`) }) // ┌─── Effect // ▼ const program = getTodo(1) ``` ## From a Callback Creates an `Effect` from a callback-based asynchronous function. Sometimes you have to work with APIs that don't support `async/await` or `Promise` and instead use the callback style. To handle callback-based APIs, Effect provides the `Effect.async` constructor. **Example** (Wrapping a Callback API) Let's wrap the `readFile` function from Node.js's `fs` module into an Effect-based API (make sure `@types/node` is installed): ```ts twoslash import { Effect } from "effect" import * as NodeFS from "node:fs" const readFile = (filename: string) => Effect.async((resume) => { NodeFS.readFile(filename, (error, data) => { if (error) { // Resume with a failed Effect if an error occurs resume(Effect.fail(error)) } else { // Resume with a succeeded Effect if successful resume(Effect.succeed(data)) } }) }) // ┌─── Effect // ▼ const program = readFile("example.txt") ``` In the above example, we manually annotate the types when calling `Effect.async`: ```ts showLineNumbers=false "" Effect.async((resume) => { // ... }) ``` because TypeScript cannot infer the type parameters for a callback based on the return value inside the callback body. Annotating the types ensures that the values provided to `resume` match the expected types. The `resume` function inside `Effect.async` should be called exactly once. Calling it more than once will result in the extra calls being ignored. **Example** (Ignoring Subsequent `resume` Calls) ```ts twoslash import { Effect } from "effect" const program = Effect.async((resume) => { resume(Effect.succeed(1)) resume(Effect.succeed(2)) // This line will be ignored }) // Run the program Effect.runPromise(program).then(console.log) // Output: 1 ``` ### Advanced Usage For more advanced use cases, `resume` can optionally return an `Effect` that will be executed if the fiber running this effect is interrupted. This can be useful in scenarios where you need to handle resource cleanup if the operation is interrupted. **Example** (Handling Interruption with Cleanup) In this example: - The `writeFileWithCleanup` function writes data to a file. - If the fiber running this effect is interrupted, the cleanup effect (which deletes the file) is executed. - This ensures that resources like open file handles are cleaned up properly when the operation is canceled. ```ts twoslash import { Effect, Fiber } from "effect" import * as NodeFS from "node:fs" // Simulates a long-running operation to write to a file const writeFileWithCleanup = (filename: string, data: string) => Effect.async((resume) => { const writeStream = NodeFS.createWriteStream(filename) // Start writing data to the file writeStream.write(data) // When the stream is finished, resume with success writeStream.on("finish", () => resume(Effect.void)) // In case of an error during writing, resume with failure writeStream.on("error", (err) => resume(Effect.fail(err))) // Handle interruption by returning a cleanup effect return Effect.sync(() => { console.log(`Cleaning up ${filename}`) NodeFS.unlinkSync(filename) }) }) const program = Effect.gen(function* () { const fiber = yield* Effect.fork( writeFileWithCleanup("example.txt", "Some long data...") ) // Simulate interrupting the fiber after 1 second yield* Effect.sleep("1 second") yield* Fiber.interrupt(fiber) // This will trigger the cleanup }) // Run the program Effect.runPromise(program) /* Output: Cleaning up example.txt */ ``` If the operation you're wrapping supports interruption, the `resume` function can receive an `AbortSignal` to handle interruption requests directly. **Example** (Handling Interruption with `AbortSignal`) ```ts twoslash import { Effect, Fiber } from "effect" // A task that supports interruption using AbortSignal const interruptibleTask = Effect.async((resume, signal) => { // Handle interruption signal.addEventListener("abort", () => { console.log("Abort signal received") clearTimeout(timeoutId) }) // Simulate a long-running task const timeoutId = setTimeout(() => { console.log("Operation completed") resume(Effect.void) }, 2000) }) const program = Effect.gen(function* () { const fiber = yield* Effect.fork(interruptibleTask) // Simulate interrupting the fiber after 1 second yield* Effect.sleep("1 second") yield* Fiber.interrupt(fiber) }) // Run the program Effect.runPromise(program) /* Output: Abort signal received */ ``` ## Suspended Effects `Effect.suspend` is used to delay the creation of an effect. It allows you to defer the evaluation of an effect until it is actually needed. The `Effect.suspend` function takes a thunk that represents the effect, and it wraps it in a suspended effect. **Syntax** ```ts showLineNumbers=false const suspendedEffect = Effect.suspend(() => effect) ``` Let's explore some common scenarios where `Effect.suspend` proves useful. ### Lazy Evaluation When you want to defer the evaluation of an effect until it is required. This can be useful for optimizing the execution of effects, especially when they are not always needed or when their computation is expensive. Also, when effects with side effects or scoped captures are created, use `Effect.suspend` to re-execute on each invocation. **Example** (Lazy Evaluation with Side Effects) ```ts twoslash import { Effect } from "effect" let i = 0 const bad = Effect.succeed(i++) const good = Effect.suspend(() => Effect.succeed(i++)) console.log(Effect.runSync(bad)) // Output: 0 console.log(Effect.runSync(bad)) // Output: 0 console.log(Effect.runSync(good)) // Output: 1 console.log(Effect.runSync(good)) // Output: 2 ``` In this example, `bad` is the result of calling `Effect.succeed(i++)` a single time, which increments the scoped variable but [returns its original value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Increment#postfix_increment). `Effect.runSync(bad)` does not result in any new computation, because `Effect.succeed(i++)` has already been called. On the other hand, each time `Effect.runSync(good)` is called, the thunk passed to `Effect.suspend()` will be executed, outputting the scoped variable's most recent value. ### Handling Circular Dependencies `Effect.suspend` is helpful in managing circular dependencies between effects, where one effect depends on another, and vice versa. For example it's fairly common for `Effect.suspend` to be used in recursive functions to escape an eager call. **Example** (Recursive Fibonacci) ```ts twoslash import { Effect } from "effect" const blowsUp = (n: number): Effect.Effect => n < 2 ? Effect.succeed(1) : Effect.zipWith(blowsUp(n - 1), blowsUp(n - 2), (a, b) => a + b) // console.log(Effect.runSync(blowsUp(32))) // crash: JavaScript heap out of memory const allGood = (n: number): Effect.Effect => n < 2 ? Effect.succeed(1) : Effect.zipWith( Effect.suspend(() => allGood(n - 1)), Effect.suspend(() => allGood(n - 2)), (a, b) => a + b ) console.log(Effect.runSync(allGood(32))) // Output: 3524578 ``` The `blowsUp` function creates a recursive Fibonacci sequence without deferring execution. Each call to `blowsUp` triggers further immediate recursive calls, rapidly increasing the JavaScript call stack size. Conversely, `allGood` avoids stack overflow by using `Effect.suspend` to defer the recursive calls. This mechanism doesn't immediately execute the recursive effects but schedules them to be run later, thus keeping the call stack shallow and preventing a crash. ### Unifying Return Type In situations where TypeScript struggles to unify the returned effect type, `Effect.suspend` can be employed to resolve this issue. **Example** (Using `Effect.suspend` to Help TypeScript Infer Types) ```ts twoslash import { Effect } from "effect" /* Without suspend, TypeScript may struggle with type inference. Inferred type: (a: number, b: number) => Effect | Effect */ const withoutSuspend = (a: number, b: number) => b === 0 ? Effect.fail(new Error("Cannot divide by zero")) : Effect.succeed(a / b) /* Using suspend to unify return types. Inferred type: (a: number, b: number) => Effect */ const withSuspend = (a: number, b: number) => Effect.suspend(() => b === 0 ? Effect.fail(new Error("Cannot divide by zero")) : Effect.succeed(a / b) ) ``` ## Cheatsheet The table provides a summary of the available constructors, along with their input and output types, allowing you to choose the appropriate function based on your needs. | API | Given | Result | | ----------------------- | ---------------------------------- | ----------------------------- | | `succeed` | `A` | `Effect` | | `fail` | `E` | `Effect` | | `sync` | `() => A` | `Effect` | | `try` | `() => A` | `Effect` | | `try` (overload) | `() => A`, `unknown => E` | `Effect` | | `promise` | `() => Promise` | `Effect` | | `tryPromise` | `() => Promise` | `Effect` | | `tryPromise` (overload) | `() => Promise`, `unknown => E` | `Effect` | | `async` | `(Effect => void) => void` | `Effect` | | `suspend` | `() => Effect` | `Effect` | For the complete list of constructors, visit the [Effect Constructors Documentation](https://effect-ts.github.io/effect/effect/Effect.ts.html#constructors). # [Importing Effect](https://effect.website/docs/getting-started/importing-effect/) ## Overview import { Aside, Tabs, TabItem } from "@astrojs/starlight/components" If you're just getting started, you might feel overwhelmed by the variety of modules and functions that Effect offers. However, rest assured that you don't need to worry about all of them right away. This page will provide a simple introduction on how to import modules and functions, and explain that installing the `effect` package is generally all you need to begin. ## Installing Effect If you haven't already installed the `effect` package, you can do so by running the following command in your terminal: ```sh showLineNumbers=false npm install effect ``` ```sh showLineNumbers=false pnpm add effect ``` ```sh showLineNumbers=false yarn add effect ``` ```sh showLineNumbers=false bun add effect ``` ```sh showLineNumbers=false deno add npm:effect ``` By installing this package, you get access to the core functionality of Effect. For detailed installation instructions for platforms like Deno or Bun, refer to the [Installation](/docs/getting-started/installation/) guide, which provides step-by-step guidance. You can also start a new Effect app using [`create-effect-app`](/docs/getting-started/create-effect-app/), which automatically sets up everything for you. ## Importing Modules and Functions Once you have installed the `effect` package, you can start using its modules and functions in your projects. Importing modules and functions is straightforward and follows the standard JavaScript/TypeScript import syntax. To import a module or a function from the `effect` package, simply use the `import` statement at the top of your file. Here's how you can import the `Effect` module: ```ts showLineNumbers=false import { Effect } from "effect" ``` Now, you have access to the Effect module, which is the heart of the Effect library. It provides various functions to create, compose, and manipulate effectful computations. ## Namespace imports In addition to importing the `Effect` module with a named import, as shown previously: ```ts showLineNumbers=false import { Effect } from "effect" ``` You can also import it using a namespace import like this: ```ts showLineNumbers=false import * as Effect from "effect/Effect" ``` Both forms of import allow you to access the functionalities provided by the `Effect` module. However an important consideration is **tree shaking**, which refers to a process that eliminates unused code during the bundling of your application. Named imports may generate tree shaking issues when a bundler doesn't support deep scope analysis. Here are some bundlers that support deep scope analysis and thus don't have issues with named imports: - Rollup - Webpack 5+ ## Functions vs Methods In the Effect ecosystem, libraries often expose functions rather than methods. This design choice is important for two key reasons: tree shakeability and extendibility. ### Tree Shakeability Tree shakeability refers to the ability of a build system to eliminate unused code during the bundling process. Functions are tree shakeable, while methods are not. When functions are used in the Effect ecosystem, only the functions that are actually imported and used in your application will be included in the final bundled code. Unused functions are automatically removed, resulting in a smaller bundle size and improved performance. On the other hand, methods are attached to objects or prototypes, and they cannot be easily tree shaken. Even if you only use a subset of methods, all methods associated with an object or prototype will be included in the bundle, leading to unnecessary code bloat. ### Extendibility Another important advantage of using functions in the Effect ecosystem is the ease of extendibility. With methods, extending the functionality of an existing API often requires modifying the prototype of the object, which can be complex and error-prone. In contrast, with functions, extending the functionality is much simpler. You can define your own "extension methods" as plain old functions without the need to modify the prototypes of objects. This promotes cleaner and more modular code, and it also allows for better compatibility with other libraries and modules. ## Commonly Used Functions As you start your adventure with Effect, you don't need to dive into every function in the `effect` package right away. Instead, focus on some commonly used functions that will provide a solid foundation for your journey into the world of Effect. In the upcoming guides, we will explore some of these essential functions, specifically those for creating and running `Effect`s and building pipelines. But before we dive into those, let's start from the very heart of Effect: understanding the `Effect` type. This will lay the groundwork for your understanding of how Effect brings composability, type safety, and error handling into your applications. So, let's take the first step and explore the fundamental concepts of the [The Effect Type](/docs/getting-started/the-effect-type/). # [Installation](https://effect.website/docs/getting-started/installation/) ## Overview import { Steps, Tabs, TabItem } from "@astrojs/starlight/components" Requirements: - TypeScript 5.4 or newer. - Node.js, Deno, and Bun are supported. ## Automatic Installation To quickly set up a new Effect application, we recommend using `create-effect-app`, which will handle all configurations for you. To create a new project, run: ```sh showLineNumbers=false npx create-effect-app@latest ``` ```sh showLineNumbers=false pnpm create effect-app@latest ``` ```sh showLineNumbers=false yarn create effect-app@latest ``` ```sh showLineNumbers=false bunx create-effect-app@latest ``` ```sh showLineNumbers=false deno init --npm effect-app@latest ``` Once you complete the prompts, `create-effect-app` will create a folder with your project name and install all required dependencies. For more details on the CLI, see the [Create Effect App](/docs/getting-started/create-effect-app/) documentation. ## Manual Installation ### Node.js Follow these steps to create a new Effect project for [Node.js](https://nodejs.org/): 1. Create a project directory and navigate into it: ```sh showLineNumbers=false mkdir hello-effect cd hello-effect ``` 2. Initialize a TypeScript project: ```sh showLineNumbers=false npm init -y npm install --save-dev typescript ``` ```sh showLineNumbers=false pnpm init pnpm add --save-dev typescript ``` ```sh showLineNumbers=false yarn init -y yarn add --dev typescript ``` This creates a `package.json` file with an initial setup for your TypeScript project. 3. Initialize TypeScript: ```sh showLineNumbers=false npm tsc --init ``` ```sh showLineNumbers=false pnpm tsc --init ``` ```sh showLineNumbers=false yarn tsc --init ``` When running this command, it will generate a `tsconfig.json` file that contains configuration options for TypeScript. One of the most important options to consider is the `strict` flag. Make sure to open the `tsconfig.json` file and verify that the value of the `strict` option is set to `true`. ```json showLineNumbers=false { "compilerOptions": { "strict": true } } ``` 4. Install the necessary package as dependency: ```sh showLineNumbers=false npm install effect ``` ```sh showLineNumbers=false pnpm add effect ``` ```sh showLineNumbers=false yarn add effect ``` This package will provide the foundational functionality for your Effect project. Let's write and run a simple program to ensure that everything is set up correctly. In your terminal, execute the following commands: ```sh showLineNumbers=false mkdir src touch src/index.ts ``` Open the `index.ts` file and add the following code: ```ts title="src/index.ts" import { Effect, Console } from "effect" const program = Console.log("Hello, World!") Effect.runSync(program) ``` Run the `index.ts` file. Here we are using [tsx](https://github.com/privatenumber/tsx) to run the `index.ts` file in the terminal: ```sh showLineNumbers=false npx tsx src/index.ts ``` You should see the message `"Hello, World!"` printed. This confirms that the program is working correctly. ### Deno Follow these steps to create a new Effect project for [Deno](https://deno.com/): 1. Create a project directory and navigate into it: ```sh showLineNumbers=false mkdir hello-effect cd hello-effect ``` 2. Initialize Deno: ```sh showLineNumbers=false deno init ``` 3. Install the necessary package as dependency: ```sh showLineNumbers=false deno add npm:effect ``` This package will provide the foundational functionality for your Effect project. Let's write and run a simple program to ensure that everything is set up correctly. Open the `main.ts` file and replace the content with the following code: ```ts title="main.ts" import { Effect, Console } from "effect" const program = Console.log("Hello, World!") Effect.runSync(program) ``` Run the `main.ts` file: ```sh showLineNumbers=false deno run main.ts ``` You should see the message `"Hello, World!"` printed. This confirms that the program is working correctly. ### Bun Follow these steps to create a new Effect project for [Bun](https://bun.sh/): 1. Create a project directory and navigate into it: ```sh showLineNumbers=false mkdir hello-effect cd hello-effect ``` 2. Initialize Bun: ```sh showLineNumbers=false bun init ``` When running this command, it will generate a `tsconfig.json` file that contains configuration options for TypeScript. One of the most important options to consider is the `strict` flag. Make sure to open the `tsconfig.json` file and verify that the value of the `strict` option is set to `true`. ```json showLineNumbers=false { "compilerOptions": { "strict": true } } ``` 3. Install the necessary package as dependency: ```sh showLineNumbers=false bun add effect ``` This package will provide the foundational functionality for your Effect project. Let's write and run a simple program to ensure that everything is set up correctly. Open the `index.ts` file and replace the content with the following code: ```ts title="index.ts" import { Effect, Console } from "effect" const program = Console.log("Hello, World!") Effect.runSync(program) ``` Run the `index.ts` file: ```sh showLineNumbers=false bun index.ts ``` You should see the message `"Hello, World!"` printed. This confirms that the program is working correctly. ### Vite + React Follow these steps to create a new Effect project for [Vite](https://vitejs.dev/guide/) + [React](https://react.dev/): 1. Scaffold your Vite project, open your terminal and run the following command: ```sh showLineNumbers=false # npm 6.x npm create vite@latest hello-effect --template react-ts # npm 7+, extra double-dash is needed npm create vite@latest hello-effect -- --template react-ts ``` ```sh showLineNumbers=false pnpm create vite@latest hello-effect -- --template react-ts ``` ```sh showLineNumbers=false yarn create vite@latest hello-effect -- --template react-ts ``` ```sh showLineNumbers=false bun create vite@latest hello-effect -- --template react-ts ``` ```sh showLineNumbers=false deno init --npm vite@latest hello-effect -- --template react-ts ``` This command will create a new Vite project with React and TypeScript template. 2. Navigate into the newly created project directory and install the required packages: ```sh showLineNumbers=false cd hello-effect npm install ``` ```sh showLineNumbers=false cd hello-effect pnpm install ``` ```sh showLineNumbers=false cd hello-effect yarn install ``` ```sh showLineNumbers=false cd hello-effect bun install ``` ```sh showLineNumbers=false cd hello-effect deno install ``` Once the packages are installed, open the `tsconfig.json` file and ensure that the value of the `strict` option is set to true. ```json showLineNumbers=false { "compilerOptions": { "strict": true } } ``` 3. Install the necessary package as dependency: ```sh showLineNumbers=false npm install effect ``` ```sh showLineNumbers=false pnpm add effect ``` ```sh showLineNumbers=false yarn add effect ``` ```sh showLineNumbers=false bun add effect ``` ```sh showLineNumbers=false deno add effect ``` This package will provide the foundational functionality for your Effect project. Now, let's write and run a simple program to ensure that everything is set up correctly. Open the `src/App.tsx` file and replace its content with the following code: ```diff lang="tsx" title="src/App.tsx" +import { useState, useMemo, useCallback } from "react" import reactLogo from "./assets/react.svg" import viteLogo from "/vite.svg" import "./App.css" +import { Effect } from "effect" function App() { const [count, setCount] = useState(0) + const task = useMemo( + () => Effect.sync(() => setCount((current) => current + 1)), + [setCount] + ) + + const increment = useCallback(() => Effect.runSync(task), [task]) return ( <>

Vite + React

+

Edit src/App.tsx and save to test HMR

Click on the Vite and React logos to learn more

) } export default App ``` After making these changes, start the development server by running the following command: ```sh showLineNumbers=false npm run dev ``` ```sh showLineNumbers=false pnpm run dev ``` ```sh showLineNumbers=false yarn run dev ``` ```sh showLineNumbers=false bun run dev ``` ```sh showLineNumbers=false deno run dev ``` Then, press **o** to open the application in your browser. When you click the button, you should see the counter increment. This confirms that the program is working correctly. # [Introduction](https://effect.website/docs/getting-started/introduction/) ## Overview Welcome to the Effect documentation! Effect is a powerful TypeScript library designed to help developers easily create complex, synchronous, and asynchronous programs. Some of the main Effect features include: | Feature | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------ | | **Concurrency** | Achieve highly-scalable, ultra low-latency applications through Effect's fiber-based concurrency model. | | **Composability** | Construct highly maintainable, readable, and flexible software through the use of small, reusable building blocks. | | **Resource Safety** | Safely manage acquisition and release of resources, even when your program fails. | | **Type Safety** | Leverage the TypeScript type system to the fullest with Effect's focus on type inference and type safety. | | **Error Handling** | Handle errors in a structured and reliable manner using Effect's built-in error handling capabilities. | | **Asynchronicity** | Write code that looks the same, whether it is synchronous or asynchronous. | | **Observability** | With full tracing capabilities, you can easily debug and monitor the execution of your Effect program. | ## How to Use These Docs The documentation is structured in a sequential manner, starting from the basics and progressing to more advanced topics. This allows you to follow along step-by-step as you build your Effect application. However, you have the flexibility to read the documentation in any order or jump directly to the pages that are relevant to your specific use case. To facilitate navigation within a page, you will find a table of contents on the right side of the screen. This allows you to easily jump between different sections of the page. ### Docs for LLMs We support the [llms.txt](https://llmstxt.org/) convention for making documentation available to large language models and the applications that make use of them. Currently, we have the following root-level files: - [/llms.txt](https://effect.website/llms.txt) — a listing of the available files - [/llms-full.txt](https://effect.website/llms-full.txt) — complete documentation for Effect - [/llms-small.txt](https://effect.website/llms-small.txt) — compressed documentation for use with smaller context windows ## Join our Community If you have questions about anything related to Effect, you're always welcome to ask our community on [Discord](https://discord.gg/effect-ts). # [Running Effects](https://effect.website/docs/getting-started/running-effects/) ## Overview import { Aside } from "@astrojs/starlight/components" To execute an effect, you can use one of the many `run` functions provided by the `Effect` module. ## runSync Executes an effect synchronously, running it immediately and returning the result. **Example** (Synchronous Logging) ```ts twoslash import { Effect } from "effect" const program = Effect.sync(() => { console.log("Hello, World!") return 1 }) const result = Effect.runSync(program) // Output: Hello, World! console.log(result) // Output: 1 ``` Use `Effect.runSync` to run an effect that does not fail and does not include any asynchronous operations. If the effect fails or involves asynchronous work, it will throw an error, and execution will stop where the failure or async operation occurs. **Example** (Incorrect Usage with Failing or Async Effects) ```ts twoslash import { Effect } from "effect" try { // Attempt to run an effect that fails Effect.runSync(Effect.fail("my error")) } catch (e) { console.error(e) } /* Output: (FiberFailure) Error: my error */ try { // Attempt to run an effect that involves async work Effect.runSync(Effect.promise(() => Promise.resolve(1))) } catch (e) { console.error(e) } /* Output: (FiberFailure) AsyncFiberException: Fiber #0 cannot be resolved synchronously. This is caused by using runSync on an effect that performs async work */ ``` ## runSyncExit Runs an effect synchronously and returns the result as an [Exit](/docs/data-types/exit/) type, which represents the outcome (success or failure) of the effect. Use `Effect.runSyncExit` to find out whether an effect succeeded or failed, including any defects, without dealing with asynchronous operations. The `Exit` type represents the result of the effect: - If the effect succeeds, the result is wrapped in a `Success`. - If it fails, the failure information is provided as a `Failure` containing a [Cause](/docs/data-types/cause/) type. **Example** (Handling Results as Exit) ```ts twoslash import { Effect } from "effect" console.log(Effect.runSyncExit(Effect.succeed(1))) /* Output: { _id: "Exit", _tag: "Success", value: 1 } */ console.log(Effect.runSyncExit(Effect.fail("my error"))) /* Output: { _id: "Exit", _tag: "Failure", cause: { _id: "Cause", _tag: "Fail", failure: "my error" } } */ ``` If the effect contains asynchronous operations, `Effect.runSyncExit` will return an `Failure` with a `Die` cause, indicating that the effect cannot be resolved synchronously. **Example** (Asynchronous Operation Resulting in Die) ```ts twoslash import { Effect } from "effect" console.log(Effect.runSyncExit(Effect.promise(() => Promise.resolve(1)))) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Die', defect: [Fiber #0 cannot be resolved synchronously. This is caused by using runSync on an effect that performs async work] { fiber: [FiberRuntime], _tag: 'AsyncFiberException', name: 'AsyncFiberException' } } } */ ``` ## runPromise Executes an effect and returns the result as a `Promise`. Use `Effect.runPromise` when you need to execute an effect and work with the result using `Promise` syntax, typically for compatibility with other promise-based code. **Example** (Running a Successful Effect as a Promise) ```ts twoslash import { Effect } from "effect" Effect.runPromise(Effect.succeed(1)).then(console.log) // Output: 1 ``` If the effect succeeds, the promise will resolve with the result. If the effect fails, the promise will reject with an error. **Example** (Handling a Failing Effect as a Rejected Promise) ```ts twoslash import { Effect } from "effect" Effect.runPromise(Effect.fail("my error")).catch(console.error) /* Output: (FiberFailure) Error: my error */ ``` ## runPromiseExit Runs an effect and returns a `Promise` that resolves to an [Exit](/docs/data-types/exit/), which represents the outcome (success or failure) of the effect. Use `Effect.runPromiseExit` when you need to determine if an effect succeeded or failed, including any defects, and you want to work with a `Promise`. The `Exit` type represents the result of the effect: - If the effect succeeds, the result is wrapped in a `Success`. - If it fails, the failure information is provided as a `Failure` containing a [Cause](/docs/data-types/cause/) type. **Example** (Handling Results as Exit) ```ts twoslash import { Effect } from "effect" Effect.runPromiseExit(Effect.succeed(1)).then(console.log) /* Output: { _id: "Exit", _tag: "Success", value: 1 } */ Effect.runPromiseExit(Effect.fail("my error")).then(console.log) /* Output: { _id: "Exit", _tag: "Failure", cause: { _id: "Cause", _tag: "Fail", failure: "my error" } } */ ``` ## runFork The foundational function for running effects, returning a "fiber" that can be observed or interrupted. `Effect.runFork` is used to run an effect in the background by creating a fiber. It is the base function for all other run functions. It starts a fiber that can be observed or interrupted. **Example** (Running an Effect in the Background) ```ts twoslash import { Effect, Console, Schedule, Fiber } from "effect" // ┌─── Effect // ▼ const program = Effect.repeat( Console.log("running..."), Schedule.spaced("200 millis") ) // ┌─── RuntimeFiber // ▼ const fiber = Effect.runFork(program) setTimeout(() => { Effect.runFork(Fiber.interrupt(fiber)) }, 500) ``` In this example, the `program` continuously logs "running..." with each repetition spaced 200 milliseconds apart. You can learn more about repetitions and scheduling in our [Introduction to Scheduling](/docs/scheduling/introduction/) guide. To stop the execution of the program, we use `Fiber.interrupt` on the fiber returned by `Effect.runFork`. This allows you to control the execution flow and terminate it when necessary. For a deeper understanding of how fibers work and how to handle interruptions, check out our guides on [Fibers](/docs/concurrency/fibers/) and [Interruptions](/docs/concurrency/basic-concurrency/#interruptions). ## Synchronous vs. Asynchronous Effects In the Effect library, there is no built-in way to determine in advance whether an effect will execute synchronously or asynchronously. While this idea was considered in earlier versions of Effect, it was ultimately not implemented for a few important reasons: 1. **Complexity:** Introducing this feature to track sync/async behavior in the type system would make Effect more complex to use and limit its composability. 2. **Safety Concerns:** We experimented with different approaches to track asynchronous Effects, but they all resulted in a worse developer experience without significantly improving safety. Even with fully synchronous types, we needed to support a `fromCallback` combinator to work with APIs using Continuation-Passing Style (CPS). However, at the type level, it's impossible to guarantee that such a function is always called immediately and not deferred. ### Best Practices for Running Effects In most cases, effects are run at the outermost parts of your application. Typically, an application built around Effect will involve a single call to the main effect. Here’s how you should approach effect execution: - Use `runPromise` or `runFork`: For most cases, asynchronous execution should be the default. These methods provide the best way to handle Effect-based workflows. - Use `runSync` only when necessary: Synchronous execution should be considered an edge case, used only in scenarios where asynchronous execution is not feasible. For example, when you are sure the effect is purely synchronous and need immediate results. ## Cheatsheet The table provides a summary of the available `run*` functions, along with their input and output types, allowing you to choose the appropriate function based on your needs. | API | Given | Result | | ---------------- | -------------- | --------------------- | | `runSync` | `Effect` | `A` | | `runSyncExit` | `Effect` | `Exit` | | `runPromise` | `Effect` | `Promise` | | `runPromiseExit` | `Effect` | `Promise>` | | `runFork` | `Effect` | `RuntimeFiber` | You can find the complete list of `run*` functions [here](https://effect-ts.github.io/effect/effect/Effect.ts.html#running-effects). # [The Effect Type](https://effect.website/docs/getting-started/the-effect-type/) ## Overview import { Aside } from "@astrojs/starlight/components" The `Effect` type is an **immutable** description of a workflow or operation that is **lazily** executed. This means that when you create an `Effect`, it doesn't run immediately, but instead defines a program that can succeed, fail, or require some additional context to complete. Here is the general form of an `Effect`: ```text showLineNumbers=false ┌─── Represents the success type │ ┌─── Represents the error type │ │ ┌─── Represents required dependencies ▼ ▼ ▼ Effect ``` This type indicates that an effect: - Succeeds and returns a value of type `Success` - Fails with an error of type `Error` - May need certain contextual dependencies of type `Requirements` to execute Conceptually, you can think of `Effect` as an effectful version of the following function type: ```ts showLineNumbers=false type Effect = ( context: Context ) => Error | Success ``` However, effects are not actually functions. They can model synchronous, asynchronous, concurrent, and resourceful computations. **Immutability**. `Effect` values are immutable, and every function in the Effect library produces a new `Effect` value. **Modeling Interactions**. These values do not perform any actions themselves, they simply model or describe effectful interactions. **Execution**. An `Effect` can be executed by the [Effect Runtime System](/docs/runtime/), which interprets it into actual interactions with the external world. Ideally, this execution happens at a single entry point in your application, such as the main function where effectful operations are initiated. ## Type Parameters The `Effect` type has three type parameters with the following meanings: | Parameter | Description | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Success** | Represents the type of value that an effect can succeed with when executed. If this type parameter is `void`, it means the effect produces no useful information, while if it is `never`, it means the effect runs forever (or until failure). | | **Error** | Represents the expected errors that can occur when executing an effect. If this type parameter is `never`, it means the effect cannot fail, because there are no values of type `never`. | | **Requirements** | Represents the contextual data required by the effect to be executed. This data is stored in a collection named `Context`. If this type parameter is `never`, it means the effect has no requirements and the `Context` collection is empty. | ## Extracting Inferred Types By using the utility types `Effect.Success`, `Effect.Error`, and `Effect.Context`, you can extract the corresponding types from an effect. **Example** (Extracting Success, Error, and Context Types) ```ts twoslash import { Effect, Context } from "effect" class SomeContext extends Context.Tag("SomeContext")() {} // Assume we have an effect that succeeds with a number, // fails with an Error, and requires SomeContext declare const program: Effect.Effect // Extract the success type, which is number type A = Effect.Effect.Success // Extract the error type, which is Error type E = Effect.Effect.Error // Extract the context type, which is SomeContext type R = Effect.Effect.Context ``` # [Using Generators](https://effect.website/docs/getting-started/using-generators/) ## Overview import { Aside, Tabs, TabItem, Badge } from "@astrojs/starlight/components" Effect offers a convenient syntax, similar to `async`/`await`, to write effectful code using [generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator). ## Understanding Effect.gen The `Effect.gen` utility simplifies the task of writing effectful code by utilizing JavaScript's generator functions. This method helps your code appear and behave more like traditional synchronous code, which enhances both readability and error management. **Example** (Performing Transactions with Discounts) Let's explore a practical program that performs a series of data transformations commonly found in application logic: ```ts twoslash import { Effect } from "effect" // Function to add a small service charge to a transaction amount const addServiceCharge = (amount: number) => amount + 1 // Function to apply a discount safely to a transaction amount const applyDiscount = ( total: number, discountRate: number ): Effect.Effect => discountRate === 0 ? Effect.fail(new Error("Discount rate cannot be zero")) : Effect.succeed(total - (total * discountRate) / 100) // Simulated asynchronous task to fetch a transaction amount from a // database const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) // Simulated asynchronous task to fetch a discount rate from a // configuration file const fetchDiscountRate = Effect.promise(() => Promise.resolve(5)) // Assembling the program using a generator function const program = Effect.gen(function* () { // Retrieve the transaction amount const transactionAmount = yield* fetchTransactionAmount // Retrieve the discount rate const discountRate = yield* fetchDiscountRate // Calculate discounted amount const discountedAmount = yield* applyDiscount( transactionAmount, discountRate ) // Apply service charge const finalAmount = addServiceCharge(discountedAmount) // Return the total amount after applying the charge return `Final amount to charge: ${finalAmount}` }) // Execute the program and log the result Effect.runPromise(program).then(console.log) // Output: Final amount to charge: 96 ``` Key steps to follow when using `Effect.gen`: - Wrap your logic in `Effect.gen` - Use `yield*` to handle effects - Return the final result ## Comparing Effect.gen with async/await If you are familiar with `async`/`await`, you may notice that the flow of writing code is similar. Let's compare the two approaches: ```ts twoslash import { Effect } from "effect" const addServiceCharge = (amount: number) => amount + 1 const applyDiscount = ( total: number, discountRate: number ): Effect.Effect => discountRate === 0 ? Effect.fail(new Error("Discount rate cannot be zero")) : Effect.succeed(total - (total * discountRate) / 100) const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) const fetchDiscountRate = Effect.promise(() => Promise.resolve(5)) export const program = Effect.gen(function* () { const transactionAmount = yield* fetchTransactionAmount const discountRate = yield* fetchDiscountRate const discountedAmount = yield* applyDiscount( transactionAmount, discountRate ) const finalAmount = addServiceCharge(discountedAmount) return `Final amount to charge: ${finalAmount}` }) ``` ```ts twoslash const addServiceCharge = (amount: number) => amount + 1 const applyDiscount = ( total: number, discountRate: number ): Promise => discountRate === 0 ? Promise.reject(new Error("Discount rate cannot be zero")) : Promise.resolve(total - (total * discountRate) / 100) const fetchTransactionAmount = Promise.resolve(100) const fetchDiscountRate = Promise.resolve(5) export const program = async function () { const transactionAmount = await fetchTransactionAmount const discountRate = await fetchDiscountRate const discountedAmount = await applyDiscount( transactionAmount, discountRate ) const finalAmount = addServiceCharge(discountedAmount) return `Final amount to charge: ${finalAmount}` } ``` It's important to note that although the code appears similar, the two programs are not identical. The purpose of comparing them side by side is just to highlight the resemblance in how they are written. ## Embracing Control Flow One significant advantage of using `Effect.gen` in conjunction with generators is its capability to employ standard control flow constructs within the generator function. These constructs include `if`/`else`, `for`, `while`, and other branching and looping mechanisms, enhancing your ability to express complex control flow logic in your code. **Example** (Using Control Flow) ```ts twoslash import { Effect } from "effect" const calculateTax = ( amount: number, taxRate: number ): Effect.Effect => taxRate > 0 ? Effect.succeed((amount * taxRate) / 100) : Effect.fail(new Error("Invalid tax rate")) const program = Effect.gen(function* () { let i = 1 while (true) { if (i === 10) { break // Break the loop when counter reaches 10 } else { if (i % 2 === 0) { // Calculate tax for even numbers console.log(yield* calculateTax(100, i)) } i++ continue } } }) Effect.runPromise(program) /* Output: 2 4 6 8 */ ``` ## How to Raise Errors The `Effect.gen` API lets you integrate error handling directly into your workflow by yielding failed effects. You can introduce errors with `Effect.fail`, as shown in the example below. **Example** (Introducing an Error into the Flow) ```ts twoslash import { Effect, Console } from "effect" const task1 = Console.log("task1...") const task2 = Console.log("task2...") const program = Effect.gen(function* () { // Perform some tasks yield* task1 yield* task2 // Introduce an error yield* Effect.fail("Something went wrong!") }) Effect.runPromise(program).then(console.log, console.error) /* Output: task1... task2... (FiberFailure) Error: Something went wrong! */ ``` ## The Role of Short-Circuiting When working with `Effect.gen`, it is important to understand how it handles errors. This API will stop execution at the **first error** it encounters and return that error. How does this affect your code? If you have several operations in sequence, once any one of them fails, the remaining operations will not run, and the error will be returned. In simpler terms, if something fails at any point, the program will stop right there and deliver the error to you. **Example** (Halting Execution at the First Error) ```ts twoslash import { Effect, Console } from "effect" const task1 = Console.log("task1...") const task2 = Console.log("task2...") const failure = Effect.fail("Something went wrong!") const task4 = Console.log("task4...") const program = Effect.gen(function* () { yield* task1 yield* task2 // The program stops here due to the error yield* failure // The following lines never run yield* task4 return "some result" }) Effect.runPromise(program).then(console.log, console.error) /* Output: task1... task2... (FiberFailure) Error: Something went wrong! */ ``` Even though execution never reaches code after a failure, TypeScript may still assume that the code below the error is reachable unless you explicitly return after the failure. For example, consider the following scenario where you want to narrow the type of a variable: **Example** (Type Narrowing without Explicit Return) ```ts twoslash import { Effect } from "effect" type User = { readonly name: string } // Imagine this function checks a database or an external service declare function getUserById(id: string): Effect.Effect function greetUser(id: string) { return Effect.gen(function* () { const user = yield* getUserById(id) if (user === undefined) { // Even though we fail here, TypeScript still thinks // 'user' might be undefined later yield* Effect.fail(`User with id ${id} not found`) } // @ts-expect-error user is possibly 'undefined'.ts(18048) return `Hello, ${user.name}!` }) } ``` In this example, TypeScript still considers `user` possibly `undefined` because there is no explicit return after the failure. To fix this, explicitly return right after calling `Effect.fail`: **Example** (Type Narrowing with Explicit Return) ```ts twoslash {15} import { Effect } from "effect" type User = { readonly name: string } declare function getUserById(id: string): Effect.Effect function greetUser(id: string) { return Effect.gen(function* () { const user = yield* getUserById(id) if (user === undefined) { // Explicitly return after failing return yield* Effect.fail(`User with id ${id} not found`) } // Now TypeScript knows that 'user' is not undefined return `Hello, ${user.name}!` }) } ``` ## Passing `this` In some cases, you might need to pass a reference to the current object (`this`) into the body of your generator function. You can achieve this by utilizing an overload that accepts the reference as the first argument: **Example** (Passing `this` to Generator) ```ts twoslash import { Effect } from "effect" class MyClass { readonly local = 1 compute = Effect.gen(this, function* () { const n = this.local + 1 yield* Effect.log(`Computed value: ${n}`) return n }) } Effect.runPromise(new MyClass().compute).then(console.log) /* Output: timestamp=... level=INFO fiber=#0 message="Computed value: 2" 2 */ ``` ## Adapter You may still come across some code snippets that use an adapter, typically indicated by `_` or `$` symbols. In earlier versions of TypeScript, the generator "adapter" function was necessary to ensure correct type inference within generators. This adapter was used to facilitate the interaction between TypeScript's type system and generator functions. **Example** (Adapter in Older Code) ```ts twoslash "$" import { Effect } from "effect" const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) // Older usage with an adapter for proper type inference const programWithAdapter = Effect.gen(function* ($) { const transactionAmount = yield* $(fetchTransactionAmount) }) // Current usage without an adapter const program = Effect.gen(function* () { const transactionAmount = yield* fetchTransactionAmount }) ``` With advances in TypeScript (v5.5+), the adapter is no longer necessary for type inference. While it remains in the codebase for backward compatibility, it is anticipated to be removed in the upcoming major release of Effect. # [Why Effect?](https://effect.website/docs/getting-started/why-effect/) ## Overview Programming is challenging. When we build libraries and apps, we look to many tools to handle the complexity and make our day-to-day more manageable. Effect presents a new way of thinking about programming in TypeScript. Effect is an ecosystem of tools that help you build better applications and libraries. As a result, you will also learn more about the TypeScript language and how to use the type system to make your programs more reliable and easier to maintain. In "typical" TypeScript, without Effect, we write code that assumes that a function is either successful or throws an exception. For example: ```ts twoslash const divide = (a: number, b: number): number => { if (b === 0) { throw new Error("Cannot divide by zero") } return a / b } ``` Based on the types, we have no idea that this function can throw an exception. We can only find out by reading the code. This may not seem like much of a problem when you only have one function in your codebase, but when you have hundreds or thousands, it really starts to add up. It's easy to forget that a function can throw an exception, and it's easy to forget to handle that exception. Often, we will do the "easiest" thing and just wrap the function in a `try/catch` block. This is a good first step to prevent your program from crashing, but it doesn't make it any easier to manage or understand our complex application/library. We can do better. One of the most important tools we have in TypeScript is the compiler. It is the first line of defense against bugs, domain errors, and general complexity. ## The Effect Pattern While Effect is a vast ecosystem of many different tools, if it had to be reduced down to just one idea, it would be the following: Effect's major unique insight is that we can use the type system to track **errors** and **context**, not only **success** values as shown in the divide example above. Here's the same divide function from above, but with the Effect pattern: ```ts twoslash import { Effect } from "effect" const divide = ( a: number, b: number ): Effect.Effect => b === 0 ? Effect.fail(new Error("Cannot divide by zero")) : Effect.succeed(a / b) ``` With this approach, the function no longer throws exceptions. Instead, errors are handled as values, which can be passed along like success values. The type signature also makes it clear: - What success value the function returns (`number`). - What error can occur (`Error`). - What additional context or dependencies are required (`never` indicates none). ```text showLineNumbers=false ┌─── Produces a value of type number │ ┌─── Fails with an Error │ │ ┌─── Requires no dependencies ▼ ▼ ▼ Effect ``` Additionally, tracking context allows you to provide additional information to your functions without having to pass in everything as an argument. For example, you can swap out implementations of live external services with mocks during your tests without changing any core business logic. ## Don't Re-Invent the Wheel Application code in TypeScript often solves the same problems over and over again. Interacting with external services, filesystems, databases, etc. are common problems for all application developers. Effect provides a rich ecosystem of libraries that provide standardized solutions to many of these problems. You can use these libraries to build your application, or you can use them to build your own libraries. Managing challenges like error handling, debugging, tracing, async/promises, retries, streaming, concurrency, caching, resource management, and a lot more are made manageable with Effect. You don't have to re-invent the solutions to these problems, or install tons of dependencies. Effect, under one umbrella, solves many of the problems that you would usually install many different dependencies with different APIs to solve. ## Solving Practical Problems Effect is heavily inspired by great work done in other languages, like Scala and Haskell. However, it's important to understand that Effect's goal is to be a practical toolkit, and it goes to great lengths to solve real, everyday problems that developers face when building applications and libraries in TypeScript. ## Enjoy Building and Learning Learning Effect is a lot of fun. Many developers in the Effect ecosystem are using Effect to solve real problems in their day-to-day work, and also experiment with cutting edge ideas for pushing TypeScript to be the most useful language it can be. You don't have to use all aspects of Effect at once, and can start with the pieces of the ecosystem that make the most sense for the problems you are solving. Effect is a toolkit, and you can pick and choose the pieces that make the most sense for your use case. However, as more and more of your codebase is using Effect, you will probably find yourself wanting to utilize more of the ecosystem! Effect's concepts may be new to you, and might not completely make sense at first. This is totally normal. Take your time with reading the docs and try to understand the core concepts - this will really pay off later on as you get into the more advanced tooling in the Effect ecosystem. The Effect community is always happy to help you learn and grow. Feel free to hop into our [Discord](https://discord.gg/effect-ts) or discuss on [GitHub](https://github.com/Effect-TS)! We are open to feedback and contributions, and are always looking for ways to improve Effect. # [Micro for Effect Users](https://effect.website/docs/micro/effect-users/) ## Overview import { Aside } from "@astrojs/starlight/components" The Micro module is designed as a lighter alternative to the standard Effect module, tailored for situations where it is beneficial to reduce the bundle size. This module is standalone and does not include more complex functionalities such as [Layer](/docs/requirements-management/layers/), [Ref](/docs/state-management/ref/), [Queue](/docs/concurrency/queue/), and [Deferred](/docs/concurrency/deferred/). This feature set makes Micro especially suitable for libraries that wish to utilize Effect functionalities while keeping the bundle size to a minimum, particularly for those aiming to provide `Promise`-based APIs. Micro also supports use cases where a client application uses Micro, and a server employs the full suite of Effect features, maintaining both compatibility and logical consistency across various application components. Integrating Micro adds a minimal footprint to your bundle, starting at **5kb gzipped**, which may increase depending on the features you use. ## Importing Micro Micro is a part of the Effect library and can be imported just like any other module: ```ts showLineNumbers=false import { Micro } from "effect" ``` You can also import it using a namespace import like this: ```ts showLineNumbers=false import * as Micro from "effect/Micro" ``` Both forms of import allow you to access the functionalities provided by the `Micro` module. However an important consideration is **tree shaking**, which refers to a process that eliminates unused code during the bundling of your application. Named imports may generate tree shaking issues when a bundler doesn't support deep scope analysis. Here are some bundlers that support deep scope analysis and thus don't have issues with named imports: - Rollup - Webpack 5+ ## Main Types ### Micro The `Micro` type uses three type parameters: ```text showLineNumbers=false ┌─── Represents the success type │ ┌─── Represents the error type │ │ ┌─── Represents required dependencies ▼ ▼ ▼ Micro ``` which mirror those of the `Effect` type. ### MicroExit The `MicroExit` type is a streamlined version of the [Exit](/docs/data-types/exit/) type, designed to capture the outcome of a `Micro` computation. It can either be successful, containing a value of type `A`, or it can fail, containing an error of type `E` wrapped in a `MicroCause`. ```ts showLineNumbers=false type MicroExit = MicroExit.Success | MicroExit.Failure ``` ### MicroCause The `MicroCause` type is a streamlined version of the [Cause](/docs/data-types/cause/) type. Similar to how `Cause` is a union of types, `MicroCause` comes in three forms: ```ts showLineNumbers=false type MicroCause = Die | Fail | Interrupt ``` | Variant | Description | | ----------- | ------------------------------------------------------------------------------------------- | | `Die` | Indicates an unforeseen defect that wasn't planned for in the system's logic. | | `Fail` | Covers anticipated errors that are recognized and typically handled within the application. | | `Interrupt` | Signifies an operation that has been purposefully stopped. | ### MicroSchedule The `MicroSchedule` type is a streamlined version of the [Schedule](/docs/scheduling/introduction/) type. ```ts showLineNumbers=false type MicroSchedule = (attempt: number, elapsed: number) => Option ``` Represents a function that can be used to calculate the delay between repeats. The function takes the current attempt number and the elapsed time since the first attempt, and returns the delay for the next attempt. If the function returns `None`, the repetition will stop. ## How to Use This Guide Below, you'll find a series of comparisons between the functionalities of `Effect` and `Micro`. Each table lists a functionality of `Effect` alongside its counterpart in `Micro`. The icons used have the following meanings: - ⚠️: The feature is available in `Micro`, but with some differences from `Effect`. - ❌: The feature is not available in `Effect`. ## Creating Effects | Effect | Micro | ⚠️ | | ---------------------- | -------------------- | ------------------------------------ | | `Effect.try` | `Micro.try` | requires a `try` block | | `Effect.tryPromise` | `Micro.tryPromise` | requires a `try` block | | `Effect.sleep` | `Micro.sleep` | only handles milliseconds | | `Effect.failCause` | `Micro.failWith` | uses `MicroCause` instead of `Cause` | | `Effect.failCauseSync` | `Micro.failWithSync` | uses `MicroCause` instead of `Cause` | | ❌ | `Micro.make` | | | ❌ | `Micro.fromOption` | | | ❌ | `Micro.fromEither` | | ## Running Effects | Effect | Micro | ⚠️ | | ----------------------- | ---------------------- | -------------------------------------------------- | | `Effect.runSyncExit` | `Micro.runSyncExit` | returns a `MicroExit` instead of an `Exit` | | `Effect.runPromiseExit` | `Micro.runPromiseExit` | returns a `MicroExit` instead of an `Exit` | | `Effect.runFork` | `Micro.runFork` | returns a `MicroFiber` instead of a `RuntimeFiber` | ### runSyncExit The `Micro.runSyncExit` function is used to execute an Effect synchronously, which means it runs immediately and returns the result as a [MicroExit](#microexit). **Example** (Handling Results as MicroExit) ```ts twoslash import { Micro } from "effect" const result1 = Micro.runSyncExit(Micro.succeed(1)) console.log(result1) /* Output: { "_id": "MicroExit", "_tag": "Success", "value": 1 } */ const result2 = Micro.runSyncExit(Micro.fail("my error")) console.log(result2) /* Output: { "_id": "MicroExit", "_tag": "Failure", "cause": { "_tag": "Fail", "traces": [], "name": "MicroCause.Fail", "error": "my error" } } */ ``` ### runPromiseExit The `Micro.runPromiseExit` function is used to execute an Effect and obtain the result as a `Promise` that resolves to a [MicroExit](#microexit). **Example** (Handling Results as MicroExit) ```ts twoslash import { Micro } from "effect" Micro.runPromiseExit(Micro.succeed(1)).then(console.log) /* Output: { "_id": "MicroExit", "_tag": "Success", "value": 1 } */ Micro.runPromiseExit(Micro.fail("my error")).then(console.log) /* Output: { "_id": "MicroExit", "_tag": "Failure", "cause": { "_tag": "Fail", "traces": [], "name": "MicroCause.Fail", "error": "my error" } } */ ``` ### runFork The `Micro.runFork` function executes the effect and return a `MicroFiber` that can be awaited, joined, or aborted. You can listen for the result by adding an observer using the `addObserver` method. **Example** (Observing an Asynchronous Effect) ```ts twoslash import { Micro } from "effect" // ┌─── MicroFiber // ▼ const fiber = Micro.succeed(42).pipe(Micro.delay(1000), Micro.runFork) // Attach an observer to log the result when the effect completes fiber.addObserver((result) => { console.log(result) }) console.log("observing...") /* Output: observing... { "_id": "MicroExit", "_tag": "Success", "value": 42 } */ ``` ## Building Pipelines | Effect | Micro | ⚠️ | | ------------------ | ----------------- | ------------------------------------------------------- | | `Effect.andThen` | `Micro.andThen` | doesn't handle `Promise` or `() => Promise` as argument | | `Effect.tap` | `Micro.tap` | doesn't handle `() => Promise` as argument | | `Effect.all` | `Micro.all` | no `batching` and `mode` options | | `Effect.forEach` | `Micro.forEach` | no `batching` option | | `Effect.filter` | `Micro.filter` | no `batching` option | | `Effect.filterMap` | `Micro.filterMap` | the filter is effectful | ## Expected Errors | Effect | Micro | ⚠️ | | ------------- | ------------ | ------------------------------------------ | | `Effect.exit` | `Micro.exit` | returns a `MicroExit` instead of an `Exit` | ## Unexpected Errors | Effect | Micro | | | ------ | -------------------- | --- | | ❌ | `Micro.catchCauseIf` | | ## Timing Out | Effect | Micro | | | ------ | --------------------- | --- | | ❌ | `Micro.timeoutOrElse` | | ## Requirements Management To access a service while using `Micro.gen`, you need to wrap the service tag using the `Micro.service` function: **Example** (Accessing a Service in `Micro.gen`) ```ts twoslash import { Micro, Context } from "effect" class Random extends Context.Tag("MyRandomService")< Random, { readonly next: Micro.Micro } >() {} const program = Micro.gen(function* () { // const random = yield* Random // this doesn't work const random = yield* Micro.service(Random) const randomNumber = yield* random.next console.log(`random number: ${randomNumber}`) }) const runnable = Micro.provideService(program, Random, { next: Micro.sync(() => Math.random()) }) Micro.runPromise(runnable) /* Example Output: random number: 0.8241872233134417 */ ``` ## Scope | Effect | Micro | ⚠️ | | ------------ | ----------------- | ------------------------------------------- | | `Scope` | `MicroScope` | returns a `MicroScope` instead of a `Scope` | | `Scope.make` | `Micro.scopeMake` | returns a `MicroScope` instead of a `Scope` | ## Retrying | Effect | Micro | ⚠️ | | -------------- | ------------- | ------------------- | | `Effect.retry` | `Micro.retry` | different `options` | ## Repetition | Effect | Micro | ⚠️ | | --------------- | ------------------ | ------------------- | | `Effect.repeat` | `Micro.repeat` | different `options` | | ❌ | `Micro.repeatExit` | | ## Timing out | Effect | Micro | | | ------ | --------------------- | --- | | ❌ | `Micro.timeoutOrElse` | | ## Sandboxing | Effect | Micro | ⚠️ | | ---------------- | --------------- | ----------------------------------------------- | | `Effect.sandbox` | `Micro.sandbox` | returns a `MicroCause` instead of `Cause` | ## Error Channel Operations | Effect | Micro | ⚠️ | | ---------------------- | ------------------------ | ------------------------------------- | | ❌ | `Micro.filterOrFailWith` | | | `Effect.tapErrorCause` | `Micro.tapErrorCause` | `MicroCause` instead of `Cause` | | ❌ | `Micro.tapCauseIf` | | | `Effect.tapDefect` | `Micro.tapDefect` | `unknown` instead of `Cause` | ## Requirements Management | Effect | Micro | ⚠️ | | ---------------- | ---------------------- | ---------------------- | | `Effect.provide` | `Micro.provideContext` | only handles `Context` | | ❌ | `Micro.provideScope` | | | ❌ | `Micro.service` | | ## Scoping, Resources and Finalization | Effect | Micro | ⚠️ | | -------------------------- | ------------------------- | ---------------------------------------- | | `Effect.addFinalizer` | `Micro.addFinalizer` | `MicroExit` instead of `Exit` and no `R` | | `Effect.acquireRelease` | `Micro.acquireRelease` | `MicroExit` instead of `Exit` | | `Effect.acquireUseRelease` | `Micro.acquireUseRelease` | `MicroExit` instead of `Exit` | | `Effect.onExit` | `Micro.onExit` | `MicroExit` instead of `Exit` | | `Effect.onError` | `Micro.onError` | uses `MicroCause` instead of `Cause` | | ❌ | `Micro.onExitIf` | | ## Concurrency | Effect | Micro | ⚠️ | | ------------------- | ------------------ | -------------------------------------- | | `Effect.fork` | `Micro.fork` | `MicroFiber` instead of `RuntimeFiber` | | `Effect.forkDaemon` | `Micro.forkDaemon` | `MicroFiber` instead of `RuntimeFiber` | | `Effect.forkIn` | `Micro.forkIn` | `MicroFiber` instead of `RuntimeFiber` | | `Effect.forkScoped` | `Micro.forkScoped` | `MicroFiber` instead of `RuntimeFiber` | # [Getting Started with Micro](https://effect.website/docs/micro/new-users/) ## Overview import { Aside, Tabs, TabItem, Steps } from "@astrojs/starlight/components" The Micro module is designed as a lighter alternative to the standard Effect module, tailored for situations where it is beneficial to reduce the bundle size. This module is standalone and does not include more complex functionalities such as [Layer](/docs/requirements-management/layers/), [Ref](/docs/state-management/ref/), [Queue](/docs/concurrency/queue/), and [Deferred](/docs/concurrency/deferred/). This feature set makes Micro especially suitable for libraries that wish to utilize Effect functionalities while keeping the bundle size to a minimum, particularly for those aiming to provide `Promise`-based APIs. Micro also supports use cases where a client application uses Micro, and a server employs the full suite of Effect features, maintaining both compatibility and logical consistency across various application components. Integrating Micro adds a minimal footprint to your bundle, starting at **5kb gzipped**, which may increase depending on the features you use. ## Importing Micro Before you start, make sure you have completed the following setup: Install the `effect` library in your project. If it is not already installed, you can add it using npm with the following command: ```sh showLineNumbers=false npm install effect ``` ```sh showLineNumbers=false pnpm add effect ``` ```sh showLineNumbers=false yarn add effect ``` ```sh showLineNumbers=false bun add effect ``` ```sh showLineNumbers=false deno add npm:effect ``` Micro is a part of the Effect library and can be imported just like any other module: ```ts showLineNumbers=false import { Micro } from "effect" ``` You can also import it using a namespace import like this: ```ts showLineNumbers=false import * as Micro from "effect/Micro" ``` Both forms of import allow you to access the functionalities provided by the `Micro` module. However an important consideration is **tree shaking**, which refers to a process that eliminates unused code during the bundling of your application. Named imports may generate tree shaking issues when a bundler doesn't support deep scope analysis. Here are some bundlers that support deep scope analysis and thus don't have issues with named imports: - Rollup - Webpack 5+ ## The Micro Type Here is the general form of a `Micro`: ```text showLineNumbers=false ┌─── Represents the success type │ ┌─── Represents the error type │ │ ┌─── Represents required dependencies ▼ ▼ ▼ Micro ``` which mirror those of the `Effect` type: | Parameter | Description | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Success** | Represents the type of value that an effect can succeed with when executed. If this type parameter is `void`, it means the effect produces no useful information, while if it is `never`, it means the effect runs forever (or until failure). | | **Error** | Represents the expected errors that can occur when executing an effect. If this type parameter is `never`, it means the effect cannot fail, because there are no values of type `never`. | | **Requirements** | Represents the contextual data required by the effect to be executed. This data is stored in a collection named `Context`. If this type parameter is `never`, it means the effect has no requirements and the `Context` collection is empty. | ## The MicroExit Type The `MicroExit` type is used to represent the result of a `Micro` computation. It can either be successful, containing a value of type `A`, or it can fail, containing an error of type `E` wrapped in a `MicroCause`. ```ts showLineNumbers=false type MicroExit = MicroExit.Success | MicroExit.Failure ``` ## The MicroCause Type The `MicroCause` type describes the different reasons why an effect may fail. `MicroCause` comes in three forms: ```ts showLineNumbers=false type MicroCause = Die | Fail | Interrupt ``` | Variant | Description | | ----------- | ------------------------------------------------------------------------------------------- | | `Die` | Indicates an unforeseen defect that wasn't planned for in the system's logic. | | `Fail` | Covers anticipated errors that are recognized and typically handled within the application. | | `Interrupt` | Signifies an operation that has been purposefully stopped. | ## Wrapping a Promise-based API with Micro This guide shows how to wrap a `Promise`-based API using the `Micro` library from Effect. We'll create a simple example that interacts with a hypothetical weather forecasting API, using Micro to handle structured error handling and execution flow. 1. **Create a Promise-based API Function** Start by defining a basic Promise-based function that simulates fetching weather data from an external service. ```ts twoslash // Simulate fetching weather data function fetchWeather(city: string): Promise { return new Promise((resolve, reject) => { setTimeout(() => { if (city === "London") { resolve("Sunny") } else { reject(new Error("Weather data not found for this location")) } }, 1_000) }) } ``` 2. **Wrap the Promise with Micro** Now, wrap the `fetchWeather` function using Micro, converting the `Promise` to a Micro effect to manage both success and failure scenarios. ```ts twoslash collapse={4-14} import { Micro } from "effect" // Simulate fetching weather data function fetchWeather(city: string): Promise { return new Promise((resolve, reject) => { setTimeout(() => { if (city === "London") { resolve("Sunny") } else { reject(new Error("Weather data not found for this location")) } }, 1_000) }) } function getWeather(city: string) { return Micro.promise(() => fetchWeather(city)) } ``` Here, `Micro.promise` transforms the `Promise` returned by `fetchWeather` into a `Micro` effect. 3. **Running the Micro Effect** Once the function is wrapped, execute the Micro effect and handle the results. **Example** (Executing the Micro Effect) ```ts twoslash collapse={4-14} import { Micro } from "effect" // Simulate fetching weather data function fetchWeather(city: string): Promise { return new Promise((resolve, reject) => { setTimeout(() => { if (city === "London") { resolve("Sunny") } else { reject(new Error("Weather data not found for this location")) } }, 1_000) }) } function getWeather(city: string) { return Micro.promise(() => fetchWeather(city)) } // ┌─── Micro // ▼ const weatherEffect = getWeather("London") Micro.runPromise(weatherEffect) .then((data) => console.log(`The weather in London is: ${data}`)) .catch((error) => console.error(`Failed to fetch weather data: ${error.message}`) ) /* Output: The weather in London is: Sunny */ ``` In the example above, `Micro.runPromise` is used to execute the `weatherEffect`, converting it back into a `Promise`, which can be managed using familiar asynchronous handling methods. For more detailed information on the effect's exit status, use `Micro.runPromiseExit`: **Example** (Inspecting Exit Status) ```ts twoslash collapse={4-14} import { Micro } from "effect" // Simulate fetching weather data function fetchWeather(city: string): Promise { return new Promise((resolve, reject) => { setTimeout(() => { if (city === "London") { resolve("Sunny") } else { reject(new Error("Weather data not found for this location")) } }, 1_000) }) } function getWeather(city: string) { return Micro.promise(() => fetchWeather(city)) } // ┌─── Micro // ▼ const weatherEffect = getWeather("London") Micro.runPromiseExit(weatherEffect).then( // ┌─── MicroExit // ▼ (exit) => console.log(exit) ) /* Output: { "_id": "MicroExit", "_tag": "Success", "value": "Sunny" } */ ``` 4. **Adding Error Handling** To further enhance the function, you might want to handle specific errors differently. Micro provides functions like `Micro.tryPromise` to handle anticipated errors gracefully. **Example** (Handling Specific Errors) ```ts twoslash collapse={4-14} import { Micro } from "effect" // Simulate fetching weather data function fetchWeather(city: string): Promise { return new Promise((resolve, reject) => { setTimeout(() => { if (city === "London") { resolve("Sunny") } else { reject(new Error("Weather data not found for this location")) } }, 1_000) }) } class WeatherError { readonly _tag = "WeatherError" constructor(readonly message: string) {} } function getWeather(city: string) { return Micro.tryPromise({ try: () => fetchWeather(city), // remap the error catch: (error) => new WeatherError(String(error)) }) } // ┌─── Micro // ▼ const weatherEffect = getWeather("Paris") Micro.runPromise(weatherEffect) .then((data) => console.log(`The weather in London is: ${data}`)) .catch((error) => console.error(`Failed to fetch weather data: ${error}`) ) /* Output: Failed to fetch weather data: MicroCause.Fail: {"_tag":"WeatherError","message":"Error: Weather data not found for this location"} */ ``` ## Expected Errors These errors, also referred to as _failures_, _typed errors_ or _recoverable errors_, are errors that developers anticipate as part of the normal program execution. They serve a similar purpose to checked exceptions and play a role in defining the program's domain and control flow. Expected errors **are tracked** at the type level by the `Micro` data type in the "Error" channel: ```text showLineNumbers=false "Error" ┌─── Represents the success type │ ┌─── Represents the error type │ │ ┌─── Represents required dependencies ▼ ▼ ▼ Micro ``` ### either The `Micro.either` function transforms an `Micro` into an effect that encapsulates both potential failure and success within an [Either](/docs/data-types/either/) data type: ```ts showLineNumbers=false Micro -> Micro, never, R> ``` This means if you have an effect with the following type: ```ts showLineNumbers=false Micro ``` and you call `Micro.either` on it, the type becomes: ```ts showLineNumbers=false Micro, never, never> ``` The resulting effect cannot fail because the potential failure is now represented within the `Either`'s `Left` type. The error type of the returned `Micro` is specified as `never`, confirming that the effect is structured to not fail. By yielding an `Either`, we gain the ability to "pattern match" on this type to handle both failure and success cases within the generator function. **Example** (Using `Micro.either` to Handle Errors) ```ts twoslash import { Micro, Either } from "effect" class HttpError { readonly _tag = "HttpError" } class ValidationError { readonly _tag = "ValidationError" } // ┌─── Micro // ▼ const program = Micro.gen(function* () { // Simulate http and validation errors if (Math.random() > 0.5) yield* Micro.fail(new HttpError()) if (Math.random() > 0.5) yield* Micro.fail(new ValidationError()) return "some result" }) // ┌─── Micro // ▼ const recovered = Micro.gen(function* () { // ┌─── Either // ▼ const failureOrSuccess = yield* Micro.either(program) return Either.match(failureOrSuccess, { // Failure case onLeft: (error) => `Recovering from ${error._tag}`, // Success case onRight: (value) => `Result is: ${value}` }) }) Micro.runPromiseExit(recovered).then(console.log) /* Example Output: { "_id": "MicroExit", "_tag": "Success", "value": "Recovering from ValidationError" } */ ``` As you can see since all errors are handled, the error type of the resulting effect `recovered` is `never`: ```ts showLineNumbers=false const recovered: Micro ``` ### catchAll The `Micro.catchAll` function allows you to catch any error that occurs in the program and provide a fallback. **Example** (Catching All Errors with `Micro.catchAll`) ```ts twoslash import { Micro } from "effect" class HttpError { readonly _tag = "HttpError" } class ValidationError { readonly _tag = "ValidationError" } // ┌─── Micro // ▼ const program = Micro.gen(function* () { // Simulate http and validation errors if (Math.random() > 0.5) yield* Micro.fail(new HttpError()) if (Math.random() > 0.5) yield* Micro.fail(new ValidationError()) return "some result" }) // ┌─── Micro // ▼ const recovered = program.pipe( Micro.catchAll((error) => Micro.succeed(`Recovering from ${error._tag}`) ) ) Micro.runPromiseExit(recovered).then(console.log) /* Example Output: { "_id": "MicroExit", "_tag": "Success", "value": "Recovering from HttpError" } */ ``` We can observe that the type in the error channel of our program has changed to `never`: ```ts showLineNumbers=false const recovered: Micro ``` indicating that all errors have been handled. ### catchTag If your program's errors are **all tagged** with a `_tag` field that acts as a discriminator you can use the `Effect.catchTag` function to catch and handle specific errors with precision. **Example** (Handling Errors by Tag with `Micro.catchTag`) ```ts twoslash import { Micro } from "effect" class HttpError { readonly _tag = "HttpError" } class ValidationError { readonly _tag = "ValidationError" } // ┌─── Micro // ▼ const program = Micro.gen(function* () { // Simulate http and validation errors if (Math.random() > 0.5) yield* Micro.fail(new HttpError()) if (Math.random() > 0.5) yield* Micro.fail(new ValidationError()) return "Success" }) // ┌─── Micro // ▼ const recovered = program.pipe( Micro.catchTag("HttpError", (_HttpError) => Micro.succeed("Recovering from HttpError") ) ) Micro.runPromiseExit(recovered).then(console.log) /* Example Output: { "_id": "MicroExit", "_tag": "Success", "value": "Recovering from HttpError" } */ ``` In the example above, the `Micro.catchTag` function allows us to handle `HttpError` specifically. If a `HttpError` occurs during the execution of the program, the provided error handler function will be invoked, and the program will proceed with the recovery logic specified within the handler. We can observe that the type in the error channel of our program has changed to only show `ValidationError`: ```ts showLineNumbers=false const recovered: Micro ``` indicating that `HttpError` has been handled. ## Unexpected Errors Unexpected errors, also referred to as _defects_, _untyped errors_, or _unrecoverable errors_, are errors that developers do not anticipate occurring during normal program execution. Unlike expected errors, which are considered part of a program's domain and control flow, unexpected errors resemble unchecked exceptions and lie outside the expected behavior of the program. Since these errors are not expected, Effect **does not track** them at the type level. However, the Effect runtime does keep track of these errors and provides several methods to aid in recovering from unexpected errors. ### die The `Micro.die` function returns an effect that throws a specified error. This function is useful for terminating a program when a defect, a critical and unexpected error, is detected in the code. **Example** (Terminating on Division by Zero with `Effect.die`) ```ts twoslash import { Micro } from "effect" const divide = (a: number, b: number): Micro.Micro => b === 0 ? Micro.die(new Error("Cannot divide by zero")) : Micro.succeed(a / b) Micro.runPromise(divide(1, 0)) /* throws: Die [(MicroCause.Die) Error]: Cannot divide by zero ...stack trace... */ ``` ### orDie The `Micro.orDie` function converts an effect's failure into a termination of the program, removing the error from the type of the effect. It is useful when you encounter failures that you do not intend to handle or recover from. **Example** (Converting Failure to Defect with `Micro.orDie`) ```ts twoslash import { Micro } from "effect" const divide = (a: number, b: number): Micro.Micro => b === 0 ? Micro.fail(new Error("Cannot divide by zero")) : Micro.succeed(a / b) // ┌─── Micro // ▼ const program = Micro.orDie(divide(1, 0)) Micro.runPromise(program) /* throws: Die [(MicroCause.Die) Error]: Cannot divide by zero ...stack trace... */ ``` ### catchAllDefect The `Micro.catchAllDefect` function allows you to recover from all defects using a provided function. **Example** (Handling All Defects with `Micro.catchAllDefect`) ```ts twoslash import { Micro } from "effect" // Helper function to log a message const log = (message: string) => Micro.sync(() => console.log(message)) // Simulating a runtime error const task = Micro.die("Boom!") const program = Micro.catchAllDefect(task, (defect) => log(`Unknown defect caught: ${defect}`) ) // We get a Right because we caught all defects Micro.runPromiseExit(program).then((exit) => console.log(exit)) /* Output: Unknown defect caught: Boom! { "_id": "MicroExit", "_tag": "Success" } */ ``` It's important to understand that `Micro.catchAllDefect` can only handle defects, not expected errors (such as those caused by `Micro.fail`) or interruptions in execution (such as when using `Micro.interrupt`). A defect refers to an error that cannot be anticipated in advance, and there is no reliable way to respond to it. As a general rule, it's recommended to let defects crash the application, as they often indicate serious issues that need to be addressed. However, in some specific cases, such as when dealing with dynamically loaded plugins, a controlled recovery approach might be necessary. For example, if our application supports runtime loading of plugins and a defect occurs within a plugin, we may choose to log the defect and then reload only the affected plugin instead of crashing the entire application. This allows for a more resilient and uninterrupted operation of the application. ## Fallback ### orElseSucceed The `Effect.orElseSucceed` function will replace the original failure with a success value, ensuring the effect cannot fail: **Example** (Replacing Failure with Success using `Micro.orElseSucceed`) ```ts twoslash import { Micro } from "effect" const validate = (age: number): Micro.Micro => { if (age < 0) { return Micro.fail("NegativeAgeError") } else if (age < 18) { return Micro.fail("IllegalAgeError") } else { return Micro.succeed(age) } } const program = Micro.orElseSucceed(validate(-1), () => 18) console.log(Micro.runSyncExit(program)) /* Output: { "_id": "MicroExit", "_tag": "Success", "value": 18 } */ ``` ## Matching ### match The `Micro.match` function lets you handle both success and failure cases without performing side effects. You provide a handler for each case. **Example** (Handling Both Success and Failure Cases) ```ts twoslash import { Micro } from "effect" const success: Micro.Micro = Micro.succeed(42) const program1 = Micro.match(success, { onFailure: (error) => `failure: ${error.message}`, onSuccess: (value) => `success: ${value}` }) // Run and log the result of the successful effect Micro.runPromise(program1).then(console.log) // Output: "success: 42" const failure: Micro.Micro = Micro.fail( new Error("Uh oh!") ) const program2 = Micro.match(failure, { onFailure: (error) => `failure: ${error.message}`, onSuccess: (value) => `success: ${value}` }) // Run and log the result of the failed effect Micro.runPromise(program2).then(console.log) // Output: "failure: Uh oh!" ``` ### matchEffect The `Micro.matchEffect` function, similar to `Micro.match`, allows you to handle both success and failure cases, but it also enables you to perform additional side effects within those handlers. **Example** (Handling Success and Failure with Side Effects) ```ts twoslash import { Micro } from "effect" // Helper function to log a message const log = (message: string) => Micro.sync(() => console.log(message)) const success: Micro.Micro = Micro.succeed(42) const failure: Micro.Micro = Micro.fail( new Error("Uh oh!") ) const program1 = Micro.matchEffect(success, { onFailure: (error) => Micro.succeed(`failure: ${error.message}`).pipe(Micro.tap(log)), onSuccess: (value) => Micro.succeed(`success: ${value}`).pipe(Micro.tap(log)) }) Micro.runSync(program1) /* Output: success: 42 */ const program2 = Micro.matchEffect(failure, { onFailure: (error) => Micro.succeed(`failure: ${error.message}`).pipe(Micro.tap(log)), onSuccess: (value) => Micro.succeed(`success: ${value}`).pipe(Micro.tap(log)) }) Micro.runSync(program2) /* Output: failure: Uh oh! */ ``` ### matchCause / matchCauseEffect The `Micro.matchCause` and `Micro.matchCauseEffect` functions allow you to handle failures more precisely by providing access to the complete cause of failure within a fiber. This makes it possible to differentiate between various failure types and respond accordingly. **Example** (Handling Different Failure Causes with `Micro.matchCauseEffect`) ```ts twoslash import { Micro } from "effect" // Helper function to log a message const log = (message: string) => Micro.sync(() => console.log(message)) const task: Micro.Micro = Micro.die("Uh oh!") const program = Micro.matchCauseEffect(task, { onFailure: (cause) => { switch (cause._tag) { case "Fail": // Handle standard failure with a logged message return log(`Fail: ${cause.error.message}`) case "Die": // Handle defects (unexpected errors) by logging the defect return log(`Die: ${cause.defect}`) case "Interrupt": // Handle interruption return log("Interrupt") } }, onSuccess: (value) => // Log success if the task completes successfully log(`succeeded with ${value} value`) }) Micro.runSync(program) // Output: "Die: Uh oh!" ``` ## Retrying ### retry The `Micro.retry` function allows you to retry a failing effect according to a defined policy. **Example** (Retrying with a Fixed Delay) ```ts twoslash import { Micro } from "effect" let count = 0 // Simulates an effect with possible failures const effect = Micro.async((resume) => { if (count <= 2) { count++ console.log("failure") resume(Micro.fail(new Error())) } else { console.log("success") resume(Micro.succeed("yay!")) } }) // Define a repetition policy using a spaced delay between retries const policy = Micro.scheduleSpaced(100) const repeated = Micro.retry(effect, { schedule: policy }) Micro.runPromise(repeated).then(console.log) /* Output: failure failure failure success yay! */ ``` ## Timing out When an operation does not finish within the specified duration, the behavior of the `Micro.timeout` depends on whether the operation is "uninterruptible". 1. **Interruptible Operation**: If the operation can be interrupted, it is terminated immediately once the timeout threshold is reached, resulting in a `TimeoutException`. ```ts twoslash import { Micro } from "effect" const task = Micro.gen(function* () { console.log("Start processing...") yield* Micro.sleep(2_000) // Simulates a delay in processing console.log("Processing complete.") return "Result" }) const timedEffect = task.pipe(Micro.timeout(1_000)) Micro.runPromiseExit(timedEffect).then(console.log) /* Output: Start processing... { "_id": "MicroExit", "_tag": "Failure", "cause": { "_tag": "Fail", "traces": [], "name": "(MicroCause.Fail) TimeoutException", "error": { "_tag": "TimeoutException" } } } */ ``` 2. **Uninterruptible Operation**: If the operation is uninterruptible, it continues until completion before the `TimeoutException` is assessed. ```ts twoslash import { Micro } from "effect" const task = Micro.gen(function* () { console.log("Start processing...") yield* Micro.sleep(2_000) // Simulates a delay in processing console.log("Processing complete.") return "Result" }) const timedEffect = task.pipe( Micro.uninterruptible, Micro.timeout(1_000) ) // Outputs a TimeoutException after the task completes, // because the task is uninterruptible Micro.runPromiseExit(timedEffect).then(console.log) /* Output: Start processing... Processing complete. { "_id": "MicroExit", "_tag": "Failure", "cause": { "_tag": "Fail", "traces": [], "name": "(MicroCause.Fail) TimeoutException", "error": { "_tag": "TimeoutException" } } } */ ``` ## Sandboxing The `Micro.sandbox` function allows you to encapsulate all the potential causes of an error in an effect. It exposes the full cause of an effect, whether it's due to a failure, defect or interruption. In simple terms, it takes an effect `Micro` and transforms it into an effect `Micro, R>` where the error channel now contains a detailed cause of the error. ```ts twoslash import { Micro } from "effect" // Helper function to log a message const log = (message: string) => Micro.sync(() => console.log(message)) // ┌─── Micro // ▼ const task = Micro.fail(new Error("Oh uh!")).pipe( Micro.as("primary result") ) // ┌─── Effect, never> // ▼ const sandboxed = Micro.sandbox(task) const program = sandboxed.pipe( Micro.catchTag("Fail", (cause) => log(`Caught a defect: ${cause.error}`).pipe( Micro.as("fallback result on expected error") ) ), Micro.catchTag("Interrupt", () => log(`Caught a defect`).pipe( Micro.as("fallback result on fiber interruption") ) ), Micro.catchTag("Die", (cause) => log(`Caught a defect: ${cause.defect}`).pipe( Micro.as("fallback result on unexpected error") ) ) ) Micro.runPromise(program).then(console.log) /* Output: Caught a defect: Error: Oh uh! fallback result on expected error */ ``` ## Inspecting Errors ### tapError Executes an effectful operation to inspect the failure of an effect without altering it. **Example** (Inspecting Errors) ```ts twoslash import { Micro } from "effect" // Helper function to log a message const log = (message: string) => Micro.sync(() => console.log(message)) // Simulate a task that fails with an error const task: Micro.Micro = Micro.fail("NetworkError") // Use tapError to log the error message when the task fails const tapping = Micro.tapError(task, (error) => log(`expected error: ${error}`) ) Micro.runFork(tapping) /* Output: expected error: NetworkError */ ``` ### tapErrorCause This function inspects the complete cause of an error, including failures and defects. **Example** (Inspecting Error Causes) ```ts twoslash import { Micro } from "effect" // Helper function to log a message const log = (message: string) => Micro.sync(() => console.log(message)) // Create a task that fails with a NetworkError const task1: Micro.Micro = Micro.fail("NetworkError") const tapping1 = Micro.tapErrorCause(task1, (cause) => log(`error cause: ${cause}`) ) Micro.runFork(tapping1) /* Output: error cause: MicroCause.Fail: NetworkError */ // Simulate a severe failure in the system const task2: Micro.Micro = Micro.die( "Something went wrong" ) const tapping2 = Micro.tapErrorCause(task2, (cause) => log(`error cause: ${cause}`) ) Micro.runFork(tapping2) /* Output: error cause: MicroCause.Die: Something went wrong */ ``` ### tapDefect Specifically inspects non-recoverable failures or defects in an effect (i.e., one or more [Die](/docs/data-types/cause/#die) causes). **Example** (Inspecting Defects) ```ts twoslash import { Micro } from "effect" // Helper function to log a message const log = (message: string) => Micro.sync(() => console.log(message)) // Simulate a task that fails with a recoverable error const task1: Micro.Micro = Micro.fail("NetworkError") // tapDefect won't log anything because NetworkError is not a defect const tapping1 = Micro.tapDefect(task1, (cause) => log(`defect: ${cause}`) ) Micro.runFork(tapping1) /* No Output */ // Simulate a severe failure in the system const task2: Micro.Micro = Micro.die( "Something went wrong" ) // Log the defect using tapDefect const tapping2 = Micro.tapDefect(task2, (cause) => log(`defect: ${cause}`) ) Micro.runFork(tapping2) /* Output: defect: Something went wrong */ ``` ## Yieldable Errors Yieldable Errors are special types of errors that can be yielded directly within a generator function using `Micro.gen`. These errors allow you to handle them intuitively, without needing to explicitly invoke `Micro.fail`. This simplifies how you manage custom errors in your code. ### Error The `Error` constructor provides a way to define a base class for yieldable errors. **Example** (Creating and Yielding a Custom Error) ```ts twoslash import { Micro } from "effect" // Define a custom error class extending Error class MyError extends Micro.Error<{ message: string }> {} export const program = Micro.gen(function* () { // Yield a custom error (equivalent to failing with MyError) yield* new MyError({ message: "Oh no!" }) }) Micro.runPromiseExit(program).then(console.log) /* Output: { "_id": "MicroExit", "_tag": "Failure", "cause": { "_tag": "Fail", "traces": [], "name": "(MicroCause.Fail) Error", "error": { "message": "Oh no!" } } } */ ``` ### TaggedError The `TaggedError` constructor lets you define custom yieldable errors with unique tags. Each error has a `_tag` property, allowing you to easily distinguish between different error types. This makes it convenient to handle specific tagged errors using functions like `Micro.catchTag`. **Example** (Handling Multiple Tagged Errors) ```ts twoslash import { Micro } from "effect" // An error with _tag: "Foo" class FooError extends Micro.TaggedError("Foo")<{ message: string }> {} // An error with _tag: "Bar" class BarError extends Micro.TaggedError("Bar")<{ randomNumber: number }> {} export const program = Micro.gen(function* () { const n = Math.random() return n > 0.5 ? "yay!" : n < 0.2 ? yield* new FooError({ message: "Oh no!" }) : yield* new BarError({ randomNumber: n }) }).pipe( // Handle different tagged errors using catchTag Micro.catchTag("Foo", (error) => Micro.succeed(`Foo error: ${error.message}`) ), Micro.catchTag("Bar", (error) => Micro.succeed(`Bar error: ${error.randomNumber}`) ) ) Micro.runPromise(program).then(console.log, console.error) /* Example Output (n < 0.2): Foo error: Oh no! */ ``` ## Requirements Management In the context of programming, a **service** refers to a reusable component or functionality that can be used by different parts of an application. Services are designed to provide specific capabilities and can be shared across multiple modules or components. Services often encapsulate common tasks or operations that are needed by different parts of an application. They can handle complex operations, interact with external systems or APIs, manage data, or perform other specialized tasks. Services are typically designed to be modular and decoupled from the rest of the application. This allows them to be easily maintained, tested, and replaced without affecting the overall functionality of the application. To create a new service, you need two things: - A unique identifier. - A type describing the possible operations of the service. ```ts twoslash import { Micro, Context } from "effect" // Declaring a tag for a service that generates random numbers class Random extends Context.Tag("MyRandomService")< Random, { readonly next: Micro.Micro } >() {} ``` Now that we have our service tag defined, let's see how we can use it by building a simple program. **Example** (Using a Custom Service in a Program) ```ts twoslash import * as Context from "effect/Context" import { Micro } from "effect" // Declaring a tag for a service that generates random numbers class Random extends Context.Tag("MyRandomService")< Random, { readonly next: Micro.Micro } >() {} // Using the service // // ┌─── Micro // ▼ const program = Micro.gen(function* () { // Access the Random service const random = yield* Micro.service(Random) // Retrieve a random number from the service const randomNumber = yield* random.next console.log(`random number: ${randomNumber}`) }) ``` It's worth noting that the type of the `program` variable includes `Random` in the `Requirements` type parameter: ```ts "Random" showLineNumbers=false const program: Micro ``` This indicates that our program requires the `Random` service to be provided in order to execute successfully. To successfully execute the program, we need to provide an actual implementation of the `Random` service. **Example** (Providing and Using a Service) ```ts twoslash import { Micro, Context } from "effect" // Declaring a tag for a service that generates random numbers class Random extends Context.Tag("MyRandomService")< Random, { readonly next: Micro.Micro } >() {} // Using the service const program = Micro.gen(function* () { // Access the Random service const random = yield* Micro.service(Random) // Retrieve a random number from the service const randomNumber = yield* random.next console.log(`random number: ${randomNumber}`) }) // Providing the implementation // // ┌─── Micro // ▼ const runnable = Micro.provideService(program, Random, { next: Micro.sync(() => Math.random()) }) Micro.runPromise(runnable) /* Example Output: random number: 0.8241872233134417 */ ``` ## Resource Management ### MicroScope In simple terms, a `MicroScope` represents the lifetime of one or more resources. When a scope is closed, the resources associated with it are guaranteed to be released. With the `MicroScope` data type, you can: - **Add finalizers**: A finalizer specifies the cleanup logic for a resource. - **Close the scope**: When the scope is closed, all resources are released, and the finalizers are executed. **Example** (Managing a Scope) ```ts twoslash import { Micro } from "effect" // Helper function to log a message const log = (message: string) => Micro.sync(() => console.log(message)) const program = // create a new scope Micro.scopeMake.pipe( // add finalizer 1 Micro.tap((scope) => scope.addFinalizer(() => log("finalizer 1"))), // add finalizer 2 Micro.tap((scope) => scope.addFinalizer(() => log("finalizer 2"))), // close the scope Micro.andThen((scope) => scope.close(Micro.exitSucceed("scope closed successfully")) ) ) Micro.runPromise(program) /* Output: finalizer 2 <-- finalizers are closed in reverse order finalizer 1 */ ``` In the above example, finalizers are added to the scope, and when the scope is closed, the finalizers are **executed in the reverse order**. This reverse order is important because it ensures that resources are released in the correct sequence. For instance, if you acquire a network connection and then access a file on a remote server, the file must be closed before the network connection to avoid errors. ### addFinalizer The `Micro.addFinalizer` function is a high-level API that allows you to add finalizers to the scope of an effect. A finalizer is a piece of code that is guaranteed to run when the associated scope is closed. The behavior of the finalizer can vary based on the `MicroExit` value, which represents how the scope was closed—whether successfully or with an error. **Example** (Adding a Finalizer on Success) ```ts twoslash import { Micro } from "effect" // Helper function to log a message const log = (message: string) => Micro.sync(() => console.log(message)) // ┌─── Micro // ▼ const program = Micro.gen(function* () { yield* Micro.addFinalizer((exit) => log(`finalizer after ${exit._tag}`)) return "some result" }) // ┌─── Micro // ▼ const runnable = Micro.scoped(program) Micro.runPromise(runnable).then(console.log, console.error) /* Output: finalizer after Success some result */ ``` Next, let's explore how things behave in the event of a failure: **Example** (Adding a Finalizer on Failure) ```ts twoslash import { Micro } from "effect" // Helper function to log a message const log = (message: string) => Micro.sync(() => console.log(message)) const program = Micro.gen(function* () { yield* Micro.addFinalizer((exit) => log(`finalizer after ${exit._tag}`)) return yield* Micro.fail("Uh oh!") }) const runnable = Micro.scoped(program) Micro.runPromiseExit(runnable).then(console.log) /* Output: finalizer after Failure { "_id": "MicroExit", "_tag": "Failure", "cause": { "_tag": "Fail", "traces": [], "name": "MicroCause.Fail", "error": "Uh oh!" } } */ ``` ### Defining Resources We can define a resource using operators like `Micro.acquireRelease(acquire, release)`, which allows us to create a scoped value from an `acquire` and `release` workflow. Every acquire release requires three actions: - **Acquiring Resource**. An effect describing the acquisition of resource. For example, opening a file. - **Using Resource**. An effect describing the actual process to produce a result. For example, counting the number of lines in a file. - **Releasing Resource**. An effect describing the final step of releasing or cleaning up the resource. For example, closing a file. The `Micro.acquireRelease` operator performs the `acquire` workflow **uninterruptibly**. This is important because if we allowed interruption during resource acquisition we could be interrupted when the resource was partially acquired. The guarantee of the `Micro.acquireRelease` operator is that if the `acquire` workflow successfully completes execution then the `release` workflow is guaranteed to be run when the `Scope` is closed. **Example** (Defining a Simple Resource) ```ts twoslash import { Micro } from "effect" // Define an interface for a resource interface MyResource { readonly contents: string readonly close: () => Promise } // Simulate resource acquisition const getMyResource = (): Promise => Promise.resolve({ contents: "lorem ipsum", close: () => new Promise((resolve) => { console.log("Resource released") resolve() }) }) // Define how the resource is acquired const acquire = Micro.tryPromise({ try: () => getMyResource().then((res) => { console.log("Resource acquired") return res }), catch: () => new Error("getMyResourceError") }) // Define how the resource is released const release = (res: MyResource) => Micro.promise(() => res.close()) // Create the resource management workflow // // ┌─── Micro // ▼ const resource = Micro.acquireRelease(acquire, release) // ┌─── Micro // ▼ const program = Micro.scoped( Micro.gen(function* () { const res = yield* resource console.log(`content is ${res.contents}`) }) ) Micro.runPromise(program) /* Resource acquired content is lorem ipsum Resource released */ ``` The `Micro.scoped` operator removes the `MicroScope` from the context, indicating that there are no longer any resources used by this workflow which require a scope. ### acquireUseRelease The `Micro.acquireUseRelease(acquire, use, release)` function is a specialized version of the `Micro.acquireRelease` function that simplifies resource management by automatically handling the scoping of resources. The main difference is that `acquireUseRelease` eliminates the need to manually call `Micro.scoped` to manage the resource's scope. It has additional knowledge about when you are done using the resource created with the `acquire` step. This is achieved by providing a `use` argument, which represents the function that operates on the acquired resource. As a result, `acquireUseRelease` can automatically determine when it should execute the release step. **Example** (Automatically Managing Resource Lifetime) ```ts twoslash import { Micro } from "effect" // Define the interface for the resource interface MyResource { readonly contents: string readonly close: () => Promise } // Simulate getting the resource const getMyResource = (): Promise => Promise.resolve({ contents: "lorem ipsum", close: () => new Promise((resolve) => { console.log("Resource released") resolve() }) }) // Define the acquisition of the resource with error handling const acquire = Micro.tryPromise({ try: () => getMyResource().then((res) => { console.log("Resource acquired") return res }), catch: () => new Error("getMyResourceError") }) // Define the release of the resource const release = (res: MyResource) => Micro.promise(() => res.close()) const use = (res: MyResource) => Micro.sync(() => console.log(`content is ${res.contents}`)) // ┌─── Micro // ▼ const program = Micro.acquireUseRelease(acquire, use, release) Micro.runPromise(program) /* Resource acquired content is lorem ipsum Resource released */ ``` ## Scheduling ### MicroSchedule The `MicroSchedule` type represents a function that can be used to calculate the delay between repeats. ```ts showLineNumbers=false type MicroSchedule = (attempt: number, elapsed: number) => Option ``` The function takes the current attempt number and the elapsed time since the first attempt, and returns the delay for the next attempt. If the function returns `None`, the repetition will stop. ### repeat The `Micro.repeat` function returns a new effect that repeats the given effect according to a specified schedule or until the first failure. **Example** (Repeating a Successful Effect) ```ts twoslash import { Micro } from "effect" // Define an effect that logs a message to the console const action = Micro.sync(() => console.log("success")) // Define a schedule that repeats the action 2 more times with a delay const policy = Micro.scheduleAddDelay(Micro.scheduleRecurs(2), () => 100) // Repeat the action according to the schedule const program = Micro.repeat(action, { schedule: policy }) Micro.runPromise(program) /* Output: success success success */ ``` **Example** (Handling Failures in Repetition) ```ts twoslash import { Micro } from "effect" let count = 0 // Define an async effect that simulates an action with potential failure const action = Micro.async((resume) => { if (count > 1) { console.log("failure") resume(Micro.fail("Uh oh!")) } else { count++ console.log("success") resume(Micro.succeed("yay!")) } }) // Define a schedule that repeats the action 2 more times with a delay const policy = Micro.scheduleAddDelay(Micro.scheduleRecurs(2), () => 100) // Repeat the action according to the schedule const program = Micro.repeat(action, { schedule: policy }) // Run the program and observe the result on failure Micro.runPromiseExit(program).then(console.log) /* Output: success success failure { "_id": "MicroExit", "_tag": "Failure", "cause": { "_tag": "Fail", "traces": [], "name": "MicroCause.Fail", "error": "Uh oh!" } } */ ``` ### Simulating Schedule Behavior This helper function, `dryRun`, demonstrates how different scheduling policies control repetition timing without executing an actual effect. By returning an array of delay intervals, it visualizes how a schedule would space repetitions. ```ts twoslash import { Option, Micro } from "effect" // Helper function to simulate and visualize a schedule's behavior const dryRun = ( schedule: Micro.MicroSchedule, // The scheduling policy to simulate maxAttempt: number = 7 // Maximum number of repetitions to simulate ): Array => { let attempt = 1 // Track the current attempt number let elapsed = 0 // Track the total elapsed time const out: Array = [] // Array to store each delay duration let duration = schedule(attempt, elapsed) // Continue until the schedule returns no delay or maxAttempt is reached while (Option.isSome(duration) && attempt <= maxAttempt) { const value = duration.value out.push(value) attempt++ elapsed += value // Get the next duration based on the current attempt // and total elapsed time duration = schedule(attempt, elapsed) } return out } ``` ### scheduleSpaced A schedule that repeats indefinitely, each repetition spaced the specified duration from the last run. **Example** (Recurring with Delay Between Executions) ```ts twoslash collapse={5-21} import { Micro } from "effect" import * as Option from "effect/Option" // Helper function to simulate and visualize a schedule's behavior const dryRun = ( schedule: Micro.MicroSchedule, maxAttempt: number = 7 ): Array => { let attempt = 1 let elapsed = 0 const out: Array = [] let duration = schedule(attempt, elapsed) while (Option.isSome(duration) && attempt <= maxAttempt) { const value = duration.value attempt++ elapsed += value out.push(value) duration = schedule(attempt, elapsed) } return out } const policy = Micro.scheduleSpaced(10) console.log(dryRun(policy)) /* Output: [ 10, 10, 10, 10, 10, 10, 10 ] */ ``` ### scheduleExponential A schedule that recurs using exponential backoff, with each delay increasing exponentially. **Example** (Exponential Backoff Schedule) ```ts twoslash collapse={5-21} import { Micro } from "effect" import * as Option from "effect/Option" // Helper function to simulate and visualize a schedule's behavior const dryRun = ( schedule: Micro.MicroSchedule, maxAttempt: number = 7 ): Array => { let attempt = 1 let elapsed = 0 const out: Array = [] let duration = schedule(attempt, elapsed) while (Option.isSome(duration) && attempt <= maxAttempt) { const value = duration.value attempt++ elapsed += value out.push(value) duration = schedule(attempt, elapsed) } return out } const policy = Micro.scheduleExponential(10) console.log(dryRun(policy)) /* Output: [ 20, 40, 80, 160, 320, 640, 1280 ] */ ``` ### scheduleUnion Combines two schedules using union. The schedule recurs as long as one of the schedules wants to, using the minimum delay between recurrences. **Example** (Union of Exponential and Spaced Schedules) ```ts twoslash collapse={5-21} import { Micro } from "effect" import * as Option from "effect/Option" // Helper function to simulate and visualize a schedule's behavior const dryRun = ( schedule: Micro.MicroSchedule, maxAttempt: number = 7 ): Array => { let attempt = 1 let elapsed = 0 const out: Array = [] let duration = schedule(attempt, elapsed) while (Option.isSome(duration) && attempt <= maxAttempt) { const value = duration.value attempt++ elapsed += value out.push(value) duration = schedule(attempt, elapsed) } return out } const policy = Micro.scheduleUnion( Micro.scheduleExponential(10), Micro.scheduleSpaced(300) ) console.log(dryRun(policy)) /* Output: [ 20, < exponential 40, 80, 160, 300, < spaced 300, 300 ] */ ``` ### scheduleIntersect Combines two schedules using intersection. The schedule recurs only if both schedules want to continue, using the maximum delay between them. **Example** (Intersection of Exponential and Recurs Schedules) ```ts twoslash collapse={5-21} import { Micro } from "effect" import * as Option from "effect/Option" // Helper function to simulate and visualize a schedule's behavior const dryRun = ( schedule: Micro.MicroSchedule, maxAttempt: number = 7 ): Array => { let attempt = 1 let elapsed = 0 const out: Array = [] let duration = schedule(attempt, elapsed) while (Option.isSome(duration) && attempt <= maxAttempt) { const value = duration.value attempt++ elapsed += value out.push(value) duration = schedule(attempt, elapsed) } return out } const policy = Micro.scheduleIntersect( Micro.scheduleExponential(10), Micro.scheduleSpaced(300) ) console.log(dryRun(policy)) /* Output: [ 300, < spaced 300, 300, 300, 320, < exponential 640, 1280 ] */ ``` ## Concurrency ### Forking Effects One of the fundamental ways to create a fiber is by forking an existing effect. When you fork an effect, it starts executing the effect on a new fiber, giving you a reference to this newly-created fiber. The following code demonstrates how to create a single fiber using the `Micro.fork` function. This fiber will execute the function `fib(100)` independently of the main fiber: **Example** (Forking a Fiber) ```ts twoslash import { Micro } from "effect" const fib = (n: number): Micro.Micro => n < 2 ? Micro.succeed(n) : Micro.zipWith(fib(n - 1), fib(n - 2), (a, b) => a + b) // ┌─── Micro, never, never> // ▼ const fib10Fiber = Micro.fork(fib(10)) ``` ### Joining Fibers A common operation with fibers is joining them using the `Micro.fiberJoin` function. This function returns a `Micro` that will succeed or fail based on the outcome of the fiber it joins: **Example** (Joining a Fiber) ```ts twoslash import { Micro } from "effect" const fib = (n: number): Micro.Micro => n < 2 ? Micro.succeed(n) : Micro.zipWith(fib(n - 1), fib(n - 2), (a, b) => a + b) // ┌─── Micro, never, never> // ▼ const fib10Fiber = Micro.fork(fib(10)) const program = Micro.gen(function* () { // Retrieve the fiber const fiber = yield* fib10Fiber // Join the fiber and get the result const n = yield* Micro.fiberJoin(fiber) console.log(n) }) Micro.runPromise(program) // Output: 55 ``` ### Awaiting Fibers Another useful function for fibers is `Micro.fiberAwait`. This function returns an effect containing a `MicroExit` value, which provides detailed information about how the fiber completed. **Example** (Awaiting Fiber Completion) ```ts twoslash import { Micro } from "effect" const fib = (n: number): Micro.Micro => n < 2 ? Micro.succeed(n) : Micro.zipWith(fib(n - 1), fib(n - 2), (a, b) => a + b) // ┌─── Micro, never, never> // ▼ const fib10Fiber = Micro.fork(fib(10)) const program = Micro.gen(function* () { // Retrieve the fiber const fiber = yield* fib10Fiber // Await its completion and get the MicroExit result const exit = yield* Micro.fiberAwait(fiber) console.log(exit) }) Micro.runPromise(program) /* Output: { "_id": "MicroExit", "_tag": "Success", "value": 55 } */ ``` ## Interruptions All effects in Effect are executed by fibers. If you didn't create the fiber yourself, it was created by an operation you're using (if it's concurrent) or by the Effect runtime system. A fiber is created any time an effect is run. When running effects concurrently, a fiber is created for each concurrent effect. To summarize: - A `Micro` is a higher-level concept that describes an effectful computation. It is lazy and immutable, meaning it represents a computation that may produce a value or fail but does not immediately execute. - A fiber, on the other hand, represents the running execution of a `Micro`. It can be interrupted or awaited to retrieve its result. Think of it as a way to control and interact with the ongoing computation. Fibers can be interrupted in various ways. Let's explore some of these scenarios and see examples of how to interrupt fibers in Effect. ### Interrupting Fibers If a fiber's result is no longer needed, it can be interrupted, which immediately terminates the fiber and safely releases all resources by running all finalizers. Similar to `.await`, `.interrupt` returns a `MicroExit` value describing how the fiber completed. **Example** (Interrupting a Fiber) ```ts twoslash import { Micro } from "effect" const program = Micro.gen(function* () { // Fork a fiber that runs indefinitely, printing "Hi!" const fiber = yield* Micro.fork( Micro.forever( Micro.sync(() => console.log("Hi!")).pipe(Micro.delay(10)) ) ) yield* Micro.sleep(30) // Interrupt the fiber yield* Micro.fiberInterrupt(fiber) }) Micro.runPromise(program) /* Output: Hi! Hi! */ ``` ### Micro.interrupt A fiber can be interrupted using the `Micro.interrupt` effect on that particular fiber. **Example** (Without Interruption) In this case, the program runs without any interruption, logging the start and completion of the task. ```ts twoslash import { Micro } from "effect" const program = Micro.gen(function* () { console.log("start") yield* Micro.sleep(2_000) console.log("done") }) Micro.runPromiseExit(program).then(console.log) /* Output: start done { "_id": "MicroExit", "_tag": "Success" } */ ``` **Example** (With Interruption) Here, the fiber is interrupted after the log `"start"` but before the `"done"` log. The `Effect.interrupt` stops the fiber, and it never reaches the final log. ```ts {6} twoslash import { Micro } from "effect" const program = Micro.gen(function* () { console.log("start") yield* Micro.sleep(2_000) yield* Micro.interrupt console.log("done") }) Micro.runPromiseExit(program).then(console.log) /* Output: start { "_id": "MicroExit", "_tag": "Failure", "cause": { "_tag": "Interrupt", "traces": [], "name": "MicroCause.Interrupt" } } */ ``` When a fiber is interrupted, the cause of the interruption is captured, including details like the fiber's ID and when it started. ### Interruption of Concurrent Effects When running multiple effects concurrently, such as with `Micro.forEach`, if one of the effects is interrupted, it causes all concurrent effects to be interrupted as well. **Example** (Interrupting Concurrent Effects) ```ts twoslash import { Micro } from "effect" const program = Micro.forEach( [1, 2, 3], (n) => Micro.gen(function* () { console.log(`start #${n}`) yield* Micro.sleep(2 * 1_000) if (n > 1) { yield* Micro.interrupt } console.log(`done #${n}`) }), { concurrency: "unbounded" } ) Micro.runPromiseExit(program).then((exit) => console.log(JSON.stringify(exit, null, 2)) ) /* Output: start #1 start #2 start #3 done #1 { "_id": "MicroExit", "_tag": "Failure", "cause": { "_tag": "Interrupt", "traces": [], "name": "MicroCause.Interrupt" } } */ ``` ## Racing The `Effect.race` function allows you to run multiple effects concurrently, returning the result of the first one that successfully completes. **Example** (Basic Race Between Effects) ```ts twoslash import { Micro } from "effect" const task1 = Micro.delay(Micro.fail("task1"), 1_000) const task2 = Micro.delay(Micro.succeed("task2"), 2_000) // Run both tasks concurrently and return // the result of the first to complete const program = Micro.race(task1, task2) Micro.runPromise(program).then(console.log) /* Output: task2 */ ``` If you want to handle the result of whichever task completes first, whether it succeeds or fails, you can use the `Micro.either` function. This function wraps the result in an [Either](/docs/data-types/either/) type, allowing you to see if the result was a success (`Right`) or a failure (`Left`): **Example** (Handling Success or Failure with Either) ```ts twoslash import { Micro } from "effect" const task1 = Micro.delay(Micro.fail("task1"), 1_000) const task2 = Micro.delay(Micro.succeed("task2"), 2_000) // Run both tasks concurrently, wrapping the result // in Either to capture success or failure const program = Micro.race(Micro.either(task1), Micro.either(task2)) Micro.runPromise(program).then(console.log) /* Output: { _id: 'Either', _tag: 'Left', left: 'task1' } */ ``` # [Logging](https://effect.website/docs/observability/logging/) ## Overview import { Aside } from "@astrojs/starlight/components" Logging is an important aspect of software development, especially for debugging and monitoring the behavior of your applications. In this section, we'll explore Effect's logging utilities and see how they compare to traditional logging methods. ## Advantages Over Traditional Logging Effect's logging utilities provide several benefits over conventional logging approaches: 1. **Dynamic Log Level Control**: With Effect's logging, you have the ability to change the log level dynamically. This means you can control which log messages get displayed based on their severity. For example, you can configure your application to log only warnings or errors, which can be extremely helpful in production environments to reduce noise. 2. **Custom Logging Output**: Effect's logging utilities allow you to change how logs are handled. You can direct log messages to various destinations, such as a service or a file, using a [custom logger](#custom-loggers). This flexibility ensures that logs are stored and processed in a way that best suits your application's requirements. 3. **Fine-Grained Logging**: Effect enables fine-grained control over logging on a per-part basis of your program. You can set different log levels for different parts of your application, tailoring the level of detail to each specific component. This can be invaluable for debugging and troubleshooting, as you can focus on the information that matters most. 4. **Environment-Based Logging**: Effect's logging utilities can be combined with deployment environments to achieve granular logging strategies. For instance, during development, you might choose to log everything at a trace level and above for detailed debugging. In contrast, your production version could be configured to log only errors or critical issues, minimizing the impact on performance and noise in production logs. 5. **Additional Features**: Effect's logging utilities come with additional features such as the ability to measure time spans, alter log levels on a per-effect basis, and integrate spans for performance monitoring. ## log The `Effect.log` function allows you to log a message at the default `INFO` level. **Example** (Logging a Simple Message) ```ts twoslash import { Effect } from "effect" const program = Effect.log("Application started") Effect.runFork(program) /* Output: timestamp=... level=INFO fiber=#0 message="Application started" */ ``` The default logger in Effect adds several useful details to each log entry: | Annotation | Description | | ----------- | --------------------------------------------------------------------------------------------------- | | `timestamp` | The timestamp when the log message was generated. | | `level` | The log level at which the message is logged (e.g., `INFO`, `ERROR`). | | `fiber` | The identifier of the [fiber](/docs/concurrency/fibers/) executing the program. | | `message` | The log message content, which can include multiple strings or values. | | `span` | (Optional) The duration of a span in milliseconds, providing insight into the timing of operations. | You can also log multiple messages at once. **Example** (Logging Multiple Messages) ```ts twoslash import { Effect } from "effect" const program = Effect.log("message1", "message2", "message3") Effect.runFork(program) /* Output: timestamp=... level=INFO fiber=#0 message=message1 message=message2 message=message3 */ ``` For added context, you can also include one or more [Cause](/docs/data-types/cause/) instances in your logs, which provide detailed error information under an additional `cause` annotation: **Example** (Logging with Causes) ```ts twoslash "cause" import { Effect, Cause } from "effect" const program = Effect.log( "message1", "message2", Cause.die("Oh no!"), Cause.die("Oh uh!") ) Effect.runFork(program) /* Output: timestamp=... level=INFO fiber=#0 message=message1 message=message2 cause="Error: Oh no! Error: Oh uh!" */ ``` ## Log Levels ### logDebug By default, `DEBUG` messages **are not displayed**. To enable `DEBUG` logs, you can adjust the logging configuration using `Logger.withMinimumLogLevel`, setting the minimum level to `LogLevel.Debug`. **Example** (Enabling Debug Logs) ```ts twoslash import { Effect, Logger, LogLevel } from "effect" const task1 = Effect.gen(function* () { yield* Effect.sleep("2 seconds") yield* Effect.logDebug("task1 done") // Log a debug message }).pipe(Logger.withMinimumLogLevel(LogLevel.Debug)) // Enable DEBUG level const task2 = Effect.gen(function* () { yield* Effect.sleep("1 second") yield* Effect.logDebug("task2 done") // This message won't be logged }) const program = Effect.gen(function* () { yield* Effect.log("start") yield* task1 yield* task2 yield* Effect.log("done") }) Effect.runFork(program) /* Output: timestamp=... level=INFO message=start timestamp=... level=DEBUG message="task1 done" <-- 2 seconds later timestamp=... level=INFO message=done <-- 1 second later */ ``` ### logInfo The `INFO` log level is displayed by default. This level is typically used for general application events or progress updates. **Example** (Logging at the Info Level) ```ts twoslash import { Effect } from "effect" const program = Effect.gen(function* () { yield* Effect.logInfo("start") yield* Effect.sleep("2 seconds") yield* Effect.sleep("1 second") yield* Effect.logInfo("done") }) Effect.runFork(program) /* Output: timestamp=... level=INFO message=start timestamp=... level=INFO message=done <-- 3 seconds later */ ``` ### logWarning The `WARN` log level is displayed by default. This level is intended for potential issues or warnings that do not immediately disrupt the flow of the program but should be monitored. **Example** (Logging at the Warning Level) ```ts twoslash import { Effect, Either } from "effect" const task = Effect.fail("Oh uh!").pipe(Effect.as(2)) const program = Effect.gen(function* () { const failureOrSuccess = yield* Effect.either(task) if (Either.isLeft(failureOrSuccess)) { yield* Effect.logWarning(failureOrSuccess.left) return 0 } else { return failureOrSuccess.right } }) Effect.runFork(program) /* Output: timestamp=... level=WARN fiber=#0 message="Oh uh!" */ ``` ### logError The `ERROR` log level is displayed by default. These messages represent issues that need to be addressed. **Example** (Logging at the Error Level) ```ts twoslash import { Effect, Either } from "effect" const task = Effect.fail("Oh uh!").pipe(Effect.as(2)) const program = Effect.gen(function* () { const failureOrSuccess = yield* Effect.either(task) if (Either.isLeft(failureOrSuccess)) { yield* Effect.logError(failureOrSuccess.left) return 0 } else { return failureOrSuccess.right } }) Effect.runFork(program) /* Output: timestamp=... level=ERROR fiber=#0 message="Oh uh!" */ ``` ### logFatal The `FATAL` log level is displayed by default. This log level is typically reserved for unrecoverable errors. **Example** (Logging at the Fatal Level) ```ts twoslash import { Effect, Either } from "effect" const task = Effect.fail("Oh uh!").pipe(Effect.as(2)) const program = Effect.gen(function* () { const failureOrSuccess = yield* Effect.either(task) if (Either.isLeft(failureOrSuccess)) { yield* Effect.logFatal(failureOrSuccess.left) return 0 } else { return failureOrSuccess.right } }) Effect.runFork(program) /* Output: timestamp=... level=FATAL fiber=#0 message="Oh uh!" */ ``` ## Custom Annotations You can enhance your log outputs by adding custom annotations using the `Effect.annotateLogs` function. This allows you to attach extra metadata to each log entry, making it easier to trace and add context to your logs. Enhance your log outputs by incorporating custom annotations with the `Effect.annotateLogs` function. This function allows you to append additional metadata to each log entry of an effect, enhancing traceability and context. ### Adding a Single Annotation You can apply a single annotation as a key/value pair to all log entries within an effect. **Example** (Single Key/Value Annotation) ```ts twoslash import { Effect } from "effect" const program = Effect.gen(function* () { yield* Effect.log("message1") yield* Effect.log("message2") }).pipe( // Annotation as key/value pair Effect.annotateLogs("key", "value") ) Effect.runFork(program) /* Output: timestamp=... level=INFO fiber=#0 message=message1 key=value timestamp=... level=INFO fiber=#0 message=message2 key=value */ ``` In this example, all logs generated within the `program` will include the annotation `key=value`. ### Annotations with Nested Effects Annotations propagate to all logs generated within nested or downstream effects. This ensures that logs from any child effects inherit the parent effect's annotations. **Example** (Propagating Annotations to Nested Effects) In this example, the annotation `key=value` is included in all logs, even those from the nested `anotherProgram` effect. ```ts twoslash import { Effect } from "effect" // Define a child program that logs an error const anotherProgram = Effect.gen(function* () { yield* Effect.logError("error1") }) // Define the main program const program = Effect.gen(function* () { yield* Effect.log("message1") yield* Effect.log("message2") yield* anotherProgram // Call the nested program }).pipe( // Attach an annotation to all logs in the scope Effect.annotateLogs("key", "value") ) Effect.runFork(program) /* Output: timestamp=... level=INFO fiber=#0 message=message1 key=value timestamp=... level=INFO fiber=#0 message=message2 key=value timestamp=... level=ERROR fiber=#0 message=error1 key=value */ ``` ### Adding Multiple Annotations You can also apply multiple annotations at once by passing an object with key/value pairs. Each key/value pair will be added to every log entry within the effect. **Example** (Multiple Annotations) ```ts twoslash import { Effect } from "effect" const program = Effect.gen(function* () { yield* Effect.log("message1") yield* Effect.log("message2") }).pipe( // Add multiple annotations Effect.annotateLogs({ key1: "value1", key2: "value2" }) ) Effect.runFork(program) /* Output: timestamp=... level=INFO fiber=#0 message=message1 key2=value2 key1=value1 timestamp=... level=INFO fiber=#0 message=message2 key2=value2 key1=value1 */ ``` In this case, each log will contain both `key1=value1` and `key2=value2`. ### Scoped Annotations If you want to limit the scope of your annotations so that they only apply to certain log entries, you can use `Effect.annotateLogsScoped`. This function confines the annotations to logs produced within a specific scope. **Example** (Scoped Annotations) ```ts twoslash import { Effect } from "effect" const program = Effect.gen(function* () { yield* Effect.log("no annotations") // No annotations yield* Effect.annotateLogsScoped({ key: "value" }) // Scoped annotation yield* Effect.log("message1") // Annotation applied yield* Effect.log("message2") // Annotation applied }).pipe( Effect.scoped, // Outside scope, no annotations Effect.andThen(Effect.log("no annotations again")) ) Effect.runFork(program) /* Output: timestamp=... level=INFO fiber=#0 message="no annotations" timestamp=... level=INFO fiber=#0 message=message1 key=value timestamp=... level=INFO fiber=#0 message=message2 key=value timestamp=... level=INFO fiber=#0 message="no annotations again" */ ``` ## Log Spans Effect provides built-in support for log spans, which allow you to measure and log the duration of specific tasks or sections of your code. This feature is helpful for tracking how long certain operations take, giving you better insights into the performance of your application. **Example** (Measuring Task Duration with a Log Span) ```ts twoslash import { Effect } from "effect" const program = Effect.gen(function* () { // Simulate a delay to represent a task taking time yield* Effect.sleep("1 second") // Log a message indicating the job is done yield* Effect.log("The job is finished!") }).pipe( // Apply a log span labeled "myspan" to measure // the duration of this operation Effect.withLogSpan("myspan") ) Effect.runFork(program) /* Output: timestamp=... level=INFO fiber=#0 message="The job is finished!" myspan=1011ms */ ``` ## Disabling Default Logging Sometimes, perhaps during test execution, you might want to disable default logging in your application. Effect provides several ways to turn off logging when needed. In this section, we'll look at different methods to disable logging in the Effect framework. **Example** (Using `Logger.withMinimumLogLevel`) One convenient way to disable logging is by using the `Logger.withMinimumLogLevel` function. This allows you to set the minimum log level to `None`, effectively turning off all log output. ```ts twoslash import { Effect, Logger, LogLevel } from "effect" const program = Effect.gen(function* () { yield* Effect.log("Executing task...") yield* Effect.sleep("100 millis") console.log("task done") }) // Default behavior: logging enabled Effect.runFork(program) /* Output: timestamp=... level=INFO fiber=#0 message="Executing task..." task done */ // Disable logging by setting minimum log level to 'None' Effect.runFork(program.pipe(Logger.withMinimumLogLevel(LogLevel.None))) /* Output: task done */ ``` **Example** (Using a Layer) Another approach to disable logging is by creating a layer that sets the minimum log level to `LogLevel.None`, effectively turning off all log output. ```ts twoslash import { Effect, Logger, LogLevel } from "effect" const program = Effect.gen(function* () { yield* Effect.log("Executing task...") yield* Effect.sleep("100 millis") console.log("task done") }) // Create a layer that disables logging const layer = Logger.minimumLogLevel(LogLevel.None) // Apply the layer to disable logging Effect.runFork(program.pipe(Effect.provide(layer))) /* Output: task done */ ``` **Example** (Using a Custom Runtime) You can also disable logging by creating a custom runtime that includes the configuration to turn off logging: ```ts twoslash import { Effect, Logger, LogLevel, ManagedRuntime } from "effect" const program = Effect.gen(function* () { yield* Effect.log("Executing task...") yield* Effect.sleep("100 millis") console.log("task done") }) // Create a custom runtime that disables logging const customRuntime = ManagedRuntime.make( Logger.minimumLogLevel(LogLevel.None) ) // Run the program using the custom runtime customRuntime.runFork(program) /* Output: task done */ ``` ## Loading the Log Level from Configuration To dynamically load the log level from a [configuration](/docs/configuration/) and apply it to your program, you can use the `Logger.minimumLogLevel` layer. This allows your application to adjust its logging behavior based on external configuration. **Example** (Loading Log Level from Configuration) ```ts twoslash import { Effect, Config, Logger, Layer, ConfigProvider, LogLevel } from "effect" // Simulate a program with logs const program = Effect.gen(function* () { yield* Effect.logError("ERROR!") yield* Effect.logWarning("WARNING!") yield* Effect.logInfo("INFO!") yield* Effect.logDebug("DEBUG!") }) // Load the log level from the configuration and apply it as a layer const LogLevelLive = Config.logLevel("LOG_LEVEL").pipe( Effect.andThen((level) => // Set the minimum log level Logger.minimumLogLevel(level) ), Layer.unwrapEffect // Convert the effect into a layer ) // Provide the loaded log level to the program const configured = Effect.provide(program, LogLevelLive) // Test the program using a mock configuration provider const test = Effect.provide( configured, Layer.setConfigProvider( ConfigProvider.fromMap( new Map([["LOG_LEVEL", LogLevel.Warning.label]]) ) ) ) Effect.runFork(test) /* Output: ... level=ERROR fiber=#0 message=ERROR! ... level=WARN fiber=#0 message=WARNING! */ ``` ## Custom loggers In this section, you'll learn how to define a custom logger and set it as the default logger in your application. Custom loggers give you control over how log messages are handled, such as routing them to external services, writing to files, or formatting logs in a specific way. ### Defining a Custom Logger You can define your own logger using the `Logger.make` function. This function allows you to specify how log messages should be processed. **Example** (Defining a Simple Custom Logger) ```ts twoslash import { Logger } from "effect" // Custom logger that outputs log messages to the console const logger = Logger.make(({ logLevel, message }) => { globalThis.console.log(`[${logLevel.label}] ${message}`) }) ``` In this example, the custom logger logs messages to the console with the log level and message formatted as `[LogLevel] Message`. ### Using a Custom Logger in a Program Let's assume you have the following tasks and a program where you log some messages: ```ts twoslash collapse={3-6} import { Effect, Logger } from "effect" // Custom logger that outputs log messages to the console const logger = Logger.make(({ logLevel, message }) => { globalThis.console.log(`[${logLevel.label}] ${message}`) }) const task1 = Effect.gen(function* () { yield* Effect.sleep("2 seconds") yield* Effect.logDebug("task1 done") }) const task2 = Effect.gen(function* () { yield* Effect.sleep("1 second") yield* Effect.logDebug("task2 done") }) const program = Effect.gen(function* () { yield* Effect.log("start") yield* task1 yield* task2 yield* Effect.log("done") }) ``` To replace the default logger with your custom logger, you can use the `Logger.replace` function. After creating a layer that replaces the default logger, you provide it to your program using `Effect.provide`. **Example** (Replacing the Default Logger with a Custom Logger) ```ts twoslash collapse={3-23} import { Effect, Logger, LogLevel } from "effect" // Custom logger that outputs log messages to the console const logger = Logger.make(({ logLevel, message }) => { globalThis.console.log(`[${logLevel.label}] ${message}`) }) const task1 = Effect.gen(function* () { yield* Effect.sleep("2 seconds") yield* Effect.logDebug("task1 done") }) const task2 = Effect.gen(function* () { yield* Effect.sleep("1 second") yield* Effect.logDebug("task2 done") }) const program = Effect.gen(function* () { yield* Effect.log("start") yield* task1 yield* task2 yield* Effect.log("done") }) // Replace the default logger with the custom logger const layer = Logger.replace(Logger.defaultLogger, logger) Effect.runFork( program.pipe( Logger.withMinimumLogLevel(LogLevel.Debug), Effect.provide(layer) ) ) ``` When you run the above program, the following log messages are printed to the console: ```ansi showLineNumbers=false [INFO] start [DEBUG] task1 done [DEBUG] task2 done [INFO] done ``` ## Built-in Loggers Effect provides several built-in loggers that you can use depending on your logging needs. These loggers offer different formats, each suited for different environments or purposes, such as development, production, or integration with external logging services. Each logger is available in two forms: the logger itself, and a layer that uses the logger and sends its output to the `Console` [default service](/docs/requirements-management/default-services/). For example, the `structuredLogger` logger generates logs in a detailed object-based format, while the `structured` layer uses the same logger and writes the output to the `Console` service. ### stringLogger (default) The `stringLogger` logger produces logs in a human-readable key-value style. This format is commonly used in development and production because it is simple and easy to read in the console. This logger does not have a corresponding layer because it is the default logger. ```ts twoslash import { Effect } from "effect" const program = Effect.log("msg1", "msg2", ["msg3", "msg4"]).pipe( Effect.delay("100 millis"), Effect.annotateLogs({ key1: "value1", key2: "value2" }), Effect.withLogSpan("myspan") ) Effect.runFork(program) ``` Output: ```ansi showLineNumbers=false timestamp=2024-12-28T10:44:31.281Z level=INFO fiber=#0 message=msg1 message=msg2 message="[ \"msg3\", \"msg4\" ]" myspan=102ms key2=value2 key1=value1 ``` ### logfmtLogger The `logfmtLogger` logger produces logs in a human-readable key-value format, similar to the [stringLogger](#stringlogger-default) logger. The main difference is that `logfmtLogger` removes extra spaces to make logs more compact. ```ts twoslash import { Effect, Logger } from "effect" const program = Effect.log("msg1", "msg2", ["msg3", "msg4"]).pipe( Effect.delay("100 millis"), Effect.annotateLogs({ key1: "value1", key2: "value2" }), Effect.withLogSpan("myspan") ) Effect.runFork(program.pipe(Effect.provide(Logger.logFmt))) ``` Output: ```ansi showLineNumbers=false timestamp=2024-12-28T10:44:31.281Z level=INFO fiber=#0 message=msg1 message=msg2 message="[\"msg3\",\"msg4\"]" myspan=102ms key2=value2 key1=value1 ``` ### prettyLogger The `prettyLogger` logger enhances log output by using color and indentation for better readability, making it particularly useful during development when visually scanning logs in the console. ```ts twoslash import { Effect, Logger } from "effect" const program = Effect.log("msg1", "msg2", ["msg3", "msg4"]).pipe( Effect.delay("100 millis"), Effect.annotateLogs({ key1: "value1", key2: "value2" }), Effect.withLogSpan("myspan") ) Effect.runFork(program.pipe(Effect.provide(Logger.pretty))) ``` Output: ```ansi showLineNumbers=false [11:37:14.265] INFO (#0) myspan=101ms: msg1 msg2 [ 'msg3', 'msg4' ] key2: value2 key1: value1 ``` ### structuredLogger The `structuredLogger` logger produces logs in a detailed object-based format. This format is helpful when you need more traceable logs, especially if other systems analyze them or store them for later review. ```ts twoslash import { Effect, Logger } from "effect" const program = Effect.log("msg1", "msg2", ["msg3", "msg4"]).pipe( Effect.delay("100 millis"), Effect.annotateLogs({ key1: "value1", key2: "value2" }), Effect.withLogSpan("myspan") ) Effect.runFork(program.pipe(Effect.provide(Logger.structured))) ``` Output: ```ansi showLineNumbers=false { message: [ 'msg1', 'msg2', [ 'msg3', 'msg4' ] ], logLevel: 'INFO', timestamp: '2024-12-28T10:44:31.281Z', cause: undefined, annotations: { key2: 'value2', key1: 'value1' }, spans: { myspan: 102 }, fiberId: '#0' } ``` | Field | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `message` | Either a single processed value or an array of processed values, depending on how many messages are logged. | | `logLevel` | A string that indicates the log level label (for example, "INFO" or "DEBUG"). | | `timestamp` | An ISO 8601 timestamp for when the log was generated (for example, "2024-01-01T00:00:00.000Z"). | | `cause` | A string that shows detailed error information, or `undefined` if no cause was provided. | | `annotations` | An object where each key is an annotation label and the corresponding value is parsed into a structured format (for instance, `{"key": "value"}`). | | `spans` | An object mapping each span label to its duration in milliseconds, measured from its start time until the moment the logger was called (for example, `{"myspan": 102}`). | | `fiberId` | The identifier of the fiber that generated this log (for example, "#0"). | ### jsonLogger The `jsonLogger` logger produces logs in JSON format. This can be useful for tools or services that parse and store JSON logs. It calls `JSON.stringify` on the object created by the [structuredLogger](#structuredlogger) logger. ```ts twoslash import { Effect, Logger } from "effect" const program = Effect.log("msg1", "msg2", ["msg3", "msg4"]).pipe( Effect.delay("100 millis"), Effect.annotateLogs({ key1: "value1", key2: "value2" }), Effect.withLogSpan("myspan") ) Effect.runFork(program.pipe(Effect.provide(Logger.json))) ``` Output: ```ansi showLineNumbers=false {"message":["msg1","msg2",["msg3","msg4"]],"logLevel":"INFO","timestamp":"2024-12-28T10:44:31.281Z","annotations":{"key2":"value2","key1":"value1"},"spans":{"myspan":102},"fiberId":"#0"} ``` ## Combine Loggers ### zip The `Logger.zip` function combines two loggers into a new logger. This new logger forwards log messages to both the original loggers. **Example** (Combining Two Loggers) ```ts import { Effect, Logger } from "effect" // Define a custom logger that logs to the console const logger = Logger.make(({ logLevel, message }) => { globalThis.console.log(`[${logLevel.label}] ${message}`) }) // Combine the default logger and the custom logger // // ┌─── Logger // ▼ const combined = Logger.zip(Logger.defaultLogger, logger) const program = Effect.log("something") Effect.runFork( program.pipe( // Replace the default logger with the combined logger Effect.provide(Logger.replace(Logger.defaultLogger, combined)) ) ) /* Output: timestamp=2025-01-09T13:50:58.655Z level=INFO fiber=#0 message=something [INFO] something */ ``` # [Supervisor](https://effect.website/docs/observability/supervisor/) ## Overview A `Supervisor` is a utility for managing fibers in Effect, allowing you to track their lifecycle (creation and termination) and producing a value of type `A` that reflects this supervision. Supervisors are useful when you need insight into or control over the behavior of fibers within your application. To create a supervisor, you can use the `Supervisor.track` function. This generates a new supervisor that keeps track of its child fibers, maintaining them in a set. This allows you to observe and monitor their status during execution. You can supervise an effect by using the `Effect.supervised` function. This function takes a supervisor as an argument and returns an effect where all child fibers forked within it are supervised by the provided supervisor. This enables you to capture detailed information about these child fibers, such as their status, through the supervisor. **Example** (Monitoring Fiber Count) In this example, we'll periodically monitor the number of fibers running in the application using a supervisor. The program calculates a Fibonacci number, spawning multiple fibers in the process, while a separate monitor tracks the fiber count. ```ts twoslash import { Effect, Supervisor, Schedule, Fiber, FiberStatus } from "effect" // Main program that monitors fibers while calculating a Fibonacci number const program = Effect.gen(function* () { // Create a supervisor to track child fibers const supervisor = yield* Supervisor.track // Start a Fibonacci calculation, supervised by the supervisor const fibFiber = yield* fib(20).pipe( Effect.supervised(supervisor), // Fork the Fibonacci effect into a fiber Effect.fork ) // Define a schedule to periodically monitor the fiber count every 500ms const policy = Schedule.spaced("500 millis").pipe( Schedule.whileInputEffect((_) => Fiber.status(fibFiber).pipe( // Continue while the Fibonacci fiber is not done Effect.andThen((status) => status !== FiberStatus.done) ) ) ) // Start monitoring the fibers, using the supervisor to track the count const monitorFiber = yield* monitorFibers(supervisor).pipe( // Repeat the monitoring according to the schedule Effect.repeat(policy), // Fork the monitoring into its own fiber Effect.fork ) // Join the monitor and Fibonacci fibers to ensure they complete yield* Fiber.join(monitorFiber) const result = yield* Fiber.join(fibFiber) console.log(`fibonacci result: ${result}`) }) // Function to monitor and log the number of active fibers const monitorFibers = ( supervisor: Supervisor.Supervisor>> ): Effect.Effect => Effect.gen(function* () { const fibers = yield* supervisor.value // Get the current set of fibers console.log(`number of fibers: ${fibers.length}`) }) // Recursive Fibonacci calculation, spawning fibers for each recursive step const fib = (n: number): Effect.Effect => Effect.gen(function* () { if (n <= 1) { return 1 } yield* Effect.sleep("500 millis") // Simulate work by delaying // Fork two fibers for the recursive Fibonacci calls const fiber1 = yield* Effect.fork(fib(n - 2)) const fiber2 = yield* Effect.fork(fib(n - 1)) // Join the fibers to retrieve their results const v1 = yield* Fiber.join(fiber1) const v2 = yield* Fiber.join(fiber2) return v1 + v2 // Combine the results }) Effect.runPromise(program) /* Output: number of fibers: 0 number of fibers: 2 number of fibers: 6 number of fibers: 14 number of fibers: 30 number of fibers: 62 number of fibers: 126 number of fibers: 254 number of fibers: 510 number of fibers: 1022 number of fibers: 2034 number of fibers: 3795 number of fibers: 5810 number of fibers: 6474 number of fibers: 4942 number of fibers: 2515 number of fibers: 832 number of fibers: 170 number of fibers: 18 number of fibers: 0 fibonacci result: 10946 */ ``` # [Metrics in Effect](https://effect.website/docs/observability/metrics/) ## Overview import { Aside } from "@astrojs/starlight/components" In complex and highly concurrent applications, managing various interconnected components can be quite challenging. Ensuring that everything runs smoothly and avoiding application downtime becomes crucial in such setups. Now, let's imagine we have a sophisticated infrastructure with numerous services. These services are replicated and distributed across our servers. However, we often lack insight into what's happening across these services, including error rates, response times, and service uptime. This lack of visibility can make it challenging to identify and address issues effectively. This is where Effect Metrics comes into play; it allows us to capture and analyze various metrics, providing valuable data for later investigation. Effect Metrics offers support for five different types of metrics: | Metric | Description | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Counter** | Counters are used to track values that increase over time, such as request counts. They help us keep tabs on how many times a specific event or action has occurred. | | **Gauge** | Gauges represent a single numerical value that can fluctuate up and down over time. They are often used to monitor metrics like memory usage, which can vary continuously. | | **Histogram** | Histograms are useful for tracking the distribution of observed values across different buckets. They are commonly used for metrics like request latencies, allowing us to understand how response times are distributed. | | **Summary** | Summaries provide insight into a sliding window of a time series and offer metrics for specific percentiles of the time series, often referred to as quantiles. This is particularly helpful for understanding latency-related metrics, such as request response times. | | **Frequency** | Frequency metrics count the occurrences of distinct string values. They are useful when you want to keep track of how often different events or conditions are happening in your application. | ## Counter In the world of metrics, a Counter is a metric that represents a single numerical value that can be both incremented and decremented over time. Think of it like a tally that keeps track of changes, such as the number of a particular type of request received by your application, whether it's increasing or decreasing. Unlike some other types of metrics (like [gauges](#gauge)), where we're interested in the value at a specific moment, with counters, we care about the cumulative value over time. This means it provides a running total of changes, which can go up and down, reflecting the dynamic nature of certain metrics. Some typical use cases for counters include: - **Request Counts**: Monitoring the number of incoming requests to your server. - **Completed Tasks**: Keeping track of how many tasks or processes have been successfully completed. - **Error Counts**: Counting the occurrences of errors in your application. ### How to Create a Counter To create a counter, you can use the `Metric.counter` constructor. **Example** (Creating a Counter) ```ts twoslash import { Metric, Effect } from "effect" const requestCount = Metric.counter("request_count", { // Optional description: "A counter for tracking requests" }) ``` Once created, the counter can accept an effect that returns a `number`, which will increment or decrement the counter. **Example** (Using a Counter) ```ts twoslash import { Metric, Effect } from "effect" const requestCount = Metric.counter("request_count") const program = Effect.gen(function* () { // Increment the counter by 1 const a = yield* requestCount(Effect.succeed(1)) // Increment the counter by 2 const b = yield* requestCount(Effect.succeed(2)) // Decrement the counter by 4 const c = yield* requestCount(Effect.succeed(-4)) // Get the current state of the counter const state = yield* Metric.value(requestCount) console.log(state) return a * b * c }) Effect.runPromise(program).then(console.log) /* Output: CounterState { count: -1, ... } -8 */ ``` ### Counter Types You can specify whether the counter tracks a `number` or `bigint`. ```ts twoslash import { Metric } from "effect" const numberCounter = Metric.counter("request_count", { description: "A counter for tracking requests" // bigint: false // default }) const bigintCounter = Metric.counter("error_count", { description: "A counter for tracking errors", bigint: true }) ``` ### Increment-Only Counters If you need a counter that only increments, you can use the `incremental: true` option. **Example** (Using an Increment-Only Counter) ```ts twoslash import { Metric, Effect } from "effect" const incrementalCounter = Metric.counter("count", { description: "a counter that only increases its value", incremental: true }) const program = Effect.gen(function* () { const a = yield* incrementalCounter(Effect.succeed(1)) const b = yield* incrementalCounter(Effect.succeed(2)) // This will have no effect on the counter const c = yield* incrementalCounter(Effect.succeed(-4)) const state = yield* Metric.value(incrementalCounter) console.log(state) return a * b * c }) Effect.runPromise(program).then(console.log) /* Output: CounterState { count: 3, ... } -8 */ ``` In this configuration, the counter only accepts positive values. Any attempts to decrement will have no effect, ensuring the counter strictly counts upwards. ### Counters With Constant Input You can configure a counter to always increment by a fixed value each time it is invoked. **Example** (Constant Input) ```ts twoslash import { Metric, Effect } from "effect" const taskCount = Metric.counter("task_count").pipe( Metric.withConstantInput(1) // Automatically increments by 1 ) const task1 = Effect.succeed(1).pipe(Effect.delay("100 millis")) const task2 = Effect.succeed(2).pipe(Effect.delay("200 millis")) const task3 = Effect.succeed(-4).pipe(Effect.delay("300 millis")) const program = Effect.gen(function* () { const a = yield* taskCount(task1) const b = yield* taskCount(task2) const c = yield* taskCount(task3) const state = yield* Metric.value(taskCount) console.log(state) return a * b * c }) Effect.runPromise(program).then(console.log) /* Output: CounterState { count: 3, ... } -8 */ ``` ## Gauge In the world of metrics, a Gauge is a metric that represents a single numerical value that can be set or adjusted. Think of it as a dynamic variable that can change over time. One common use case for a gauge is to monitor something like the current memory usage of your application. Unlike counters, where we're interested in cumulative values over time, with gauges, our focus is on the current value at a specific point in time. Gauges are the best choice when you want to monitor values that can both increase and decrease, and you're not interested in tracking their rates of change. In other words, gauges help us measure things that have a specific value at a particular moment. Some typical use cases for gauges include: - **Memory Usage**: Keeping an eye on how much memory your application is using right now. - **Queue Size**: Monitoring the current size of a queue where tasks are waiting to be processed. - **In-Progress Request Counts**: Tracking the number of requests currently being handled by your server. - **Temperature**: Measuring the current temperature, which can fluctuate up and down. ### How to Create a Gauge To create a gauge, you can use the `Metric.gauge` constructor. **Example** (Creating a Gauge) ```ts twoslash import { Metric } from "effect" const memory = Metric.gauge("memory_usage", { // Optional description: "A gauge for memory usage" }) ``` Once created, a gauge can be updated by passing an effect that produces the value you want to set for the gauge. **Example** (Using a Gauge) ```ts twoslash import { Metric, Effect, Random } from "effect" // Create a gauge to track temperature const temperature = Metric.gauge("temperature") // Simulate fetching a random temperature const getTemperature = Effect.gen(function* () { // Get a random temperature between -10 and 10 const t = yield* Random.nextIntBetween(-10, 10) console.log(`new temperature: ${t}`) return t }) // Program that updates the gauge multiple times const program = Effect.gen(function* () { const series: Array = [] // Update the gauge with new temperature readings series.push(yield* temperature(getTemperature)) series.push(yield* temperature(getTemperature)) series.push(yield* temperature(getTemperature)) // Retrieve the current state of the gauge const state = yield* Metric.value(temperature) console.log(state) return series }) Effect.runPromise(program).then(console.log) /* Example Output: new temperature: 9 new temperature: -9 new temperature: 2 GaugeState { value: 2, // the most recent value set in the gauge ... } [ 9, -9, 2 ] */ ``` ### Gauge Types You can specify whether the gauge tracks a `number` or `bigint`. ```ts twoslash import { Metric } from "effect" const numberGauge = Metric.gauge("memory_usage", { description: "A gauge for memory usage" // bigint: false // default }) const bigintGauge = Metric.gauge("cpu_load", { description: "A gauge for CPU load", bigint: true }) ``` ## Histogram A Histogram is a metric used to analyze how numerical values are distributed over time. Instead of focusing on individual data points, a histogram groups values into predefined ranges, called **buckets**, and tracks how many values fall into each range. When a value is recorded, it gets assigned to one of the histogram's buckets based on its range. Each bucket has an upper boundary, and the count for that bucket is increased if the value is less than or equal to its boundary. Once recorded, the individual value is discarded, and the focus shifts to how many values have fallen into each bucket. Histograms also track: - **Total Count**: The number of values that have been observed. - **Sum**: The sum of all the observed values. - **Min**: The smallest observed value. - **Max**: The largest observed value. Histograms are especially useful for calculating percentiles, which can help you estimate specific points in a dataset by analyzing how many values are in each bucket. This concept is inspired by [Prometheus](https://prometheus.io/docs/concepts/metric_types#histogram), a well-known monitoring and alerting toolkit. Histograms are particularly useful in performance analysis and system monitoring. By examining how response times, latencies, or other metrics are distributed, you can gain valuable insights into your system's behavior. This data helps you identify outliers, performance bottlenecks, or trends that may require optimization. Common use cases for histograms include: - **Percentile Estimation**: Histograms allow you to approximate percentiles of observed values, like the 95th percentile of response times. - **Known Ranges**: If you can estimate the range of values in advance, histograms can organize the data into predefined buckets for better analysis. - **Performance Metrics**: Use histograms to track metrics like request latencies, memory usage, or throughput over time. - **Aggregation**: Histograms can be aggregated across multiple instances, making them ideal for distributed systems where you need to collect data from different sources. **Example** (Histogram With Linear Buckets) In this example, we define a histogram with linear buckets, where the values range from `0` to `100` in increments of `10`. Additionally, we include a final bucket for values greater than `100`, referred to as the "Infinity" bucket. This configuration is useful for tracking numeric values, like request latencies, within specific ranges. The program generates random numbers between `1` and `120`, records them in the histogram, and then prints the histogram's state, showing the count of values that fall into each bucket. ```ts twoslash import { Effect, Metric, MetricBoundaries, Random } from "effect" // Define a histogram to track request latencies, with linear buckets const latency = Metric.histogram( "request_latency", // Buckets from 0-100, with an extra Infinity bucket MetricBoundaries.linear({ start: 0, width: 10, count: 11 }), // Optional "Measures the distribution of request latency." ) const program = Effect.gen(function* () { // Generate 100 random values and record them in the histogram yield* latency(Random.nextIntBetween(1, 120)).pipe(Effect.repeatN(99)) // Fetch and display the histogram's state const state = yield* Metric.value(latency) console.log(state) }) Effect.runPromise(program) /* Example Output: HistogramState { buckets: [ [ 0, 0 ], // No values in the 0-10 range [ 10, 7 ], // 7 values in the 10-20 range [ 20, 11 ], // 11 values in the 20-30 range [ 30, 20 ], // 20 values in the 30-40 range [ 40, 27 ], // and so on... [ 50, 38 ], [ 60, 53 ], [ 70, 64 ], [ 80, 73 ], [ 90, 84 ], [ Infinity, 100 ] // All 100 values have been recorded ], count: 100, // Total count of observed values min: 1, // Smallest observed value max: 119, // Largest observed value sum: 5980, // Sum of all observed values ... } */ ``` ### Timer Metric In this example, we demonstrate how to use a timer metric to track the duration of specific workflows. The timer captures how long certain tasks take to execute, storing this information in a histogram, which provides insights into the distribution of these durations. We generate random values to simulate varying wait times, record the durations in the timer, and then print out the histogram's state. **Example** (Tracking Workflow Durations with a Timer Metric) ```ts twoslash import { Metric, Array, Random, Effect } from "effect" // Create a timer metric with predefined boundaries from 1 to 10 const timer = Metric.timerWithBoundaries("timer", Array.range(1, 10)) // Define a task that simulates random wait times const task = Effect.gen(function* () { // Generate a random value between 1 and 10 const n = yield* Random.nextIntBetween(1, 10) // Simulate a delay based on the random value yield* Effect.sleep(`${n} millis`) }) const program = Effect.gen(function* () { // Track the duration of the task and repeat it 100 times yield* Metric.trackDuration(task, timer).pipe(Effect.repeatN(99)) // Retrieve and print the current state of the timer histogram const state = yield* Metric.value(timer) console.log(state) }) Effect.runPromise(program) /* Example Output: HistogramState { buckets: [ [ 1, 3 ], // 3 tasks completed in <= 1 ms [ 2, 13 ], // 13 tasks completed in <= 2 ms [ 3, 17 ], // and so on... [ 4, 26 ], [ 5, 35 ], [ 6, 43 ], [ 7, 53 ], [ 8, 56 ], [ 9, 65 ], [ 10, 72 ], [ Infinity, 100 ] // All 100 tasks have completed ], count: 100, // Total number of tasks observed min: 0.25797, // Shortest task duration in milliseconds max: 12.25421, // Longest task duration in milliseconds sum: 683.0266810000002, // Total time spent across all tasks ... } */ ``` ## Summary A Summary is a metric that gives insights into a series of data points by calculating specific percentiles. Percentiles help us understand how data is distributed. For instance, if you're tracking response times for requests over the past hour, you may want to examine key percentiles such as the 50th, 90th, 95th, or 99th to better understand your system's performance. Summaries are similar to histograms in that they observe `number` values, but with a different approach. Instead of immediately sorting values into buckets and discarding them, a summary holds onto the observed values in memory. However, to avoid storing too much data, summaries use two parameters: - **maxAge**: The maximum age a value can have before it's discarded. - **maxSize**: The maximum number of values stored in the summary. This creates a sliding window of recent values, so the summary always represents a fixed number of the most recent observations. Summaries are commonly used to calculate **quantiles** over this sliding window. A **quantile** is a number between `0` and `1` that represents the percentage of values less than or equal to a certain threshold. For example, a quantile of `0.5` (or 50th percentile) is the **median** value, while `0.95` (or 95th percentile) would represent the value below which 95% of the observed data falls. Quantiles are helpful for monitoring important performance metrics, such as latency, and for ensuring that your system meets performance goals (like Service Level Agreements, or SLAs). The Effect Metrics API also allows you to configure summaries with an **error margin**. This margin introduces a range of acceptable values for quantiles, improving the accuracy of the result. Summaries are particularly useful in cases where: - The range of values you're observing is not known or estimated in advance, making histograms less practical. - You don't need to aggregate data across multiple instances or average results. Summaries calculate their results on the application side, meaning they focus on the specific instance where they are used. **Example** (Creating and Using a Summary) In this example, we will create a summary to track response times. The summary will: - Hold up to `100` samples. - Discard samples older than `1 day`. - Have a `3%` error margin when calculating quantiles. - Report the `10%`, `50%`, and `90%` quantiles, which help track response time distributions. We'll apply the summary to an effect that generates random integers, simulating response times. ```ts twoslash import { Metric, Random, Effect } from "effect" // Define the summary for response times const responseTimeSummary = Metric.summary({ name: "response_time_summary", // Name of the summary metric maxAge: "1 day", // Maximum sample age maxSize: 100, // Maximum number of samples to retain error: 0.03, // Error margin for quantile calculation quantiles: [0.1, 0.5, 0.9], // Quantiles to observe (10%, 50%, 90%) // Optional description: "Measures the distribution of response times" }) const program = Effect.gen(function* () { // Record 100 random response times between 1 and 120 ms yield* responseTimeSummary(Random.nextIntBetween(1, 120)).pipe( Effect.repeatN(99) ) // Retrieve and log the current state of the summary const state = yield* Metric.value(responseTimeSummary) console.log("%o", state) }) Effect.runPromise(program) /* Example Output: SummaryState { error: 0.03, // Error margin used for quantile calculation quantiles: [ [ 0.1, { _id: 'Option', _tag: 'Some', value: 17 } ], // 10th percentile: 17 ms [ 0.5, { _id: 'Option', _tag: 'Some', value: 62 } ], // 50th percentile (median): 62 ms [ 0.9, { _id: 'Option', _tag: 'Some', value: 109 } ] // 90th percentile: 109 ms ], count: 100, // Total number of samples recorded min: 4, // Minimum observed value max: 119, // Maximum observed value sum: 6058, // Sum of all recorded values ... } */ ``` ## Frequency Frequencies are metrics that help count the occurrences of specific values. Think of them as a set of counters, each associated with a unique value. When new values are observed, the frequency metric automatically creates new counters for those values. Frequencies are particularly useful for tracking how often distinct string values occur. Some example use cases include: - Counting the number of invocations for each service in an application, where each service has a logical name. - Monitoring how frequently different types of failures occur. **Example** (Tracking Error Occurrences) In this example, we'll create a `Frequency` to observe how often different error codes occur. This can be applied to effects that return a `string` value: ```ts twoslash import { Metric, Random, Effect } from "effect" // Define a frequency metric to track errors const errorFrequency = Metric.frequency("error_frequency", { // Optional description: "Counts the occurrences of errors." }) const task = Effect.gen(function* () { const n = yield* Random.nextIntBetween(1, 10) return `Error-${n}` }) // Program that simulates random errors and tracks their occurrences const program = Effect.gen(function* () { yield* errorFrequency(task).pipe(Effect.repeatN(99)) // Retrieve and log the current state of the summary const state = yield* Metric.value(errorFrequency) console.log("%o", state) }) Effect.runPromise(program) /* Example Output: FrequencyState { occurrences: Map(9) { 'Error-7' => 12, 'Error-2' => 12, 'Error-4' => 14, 'Error-1' => 14, 'Error-9' => 8, 'Error-6' => 11, 'Error-5' => 9, 'Error-3' => 14, 'Error-8' => 6 }, ... } */ ``` ## Tagging Metrics Tags are key-value pairs you can add to metrics to provide additional context. They help categorize and filter metrics, making it easier to analyze specific aspects of your application's performance or behavior. When creating metrics, you can add tags to them. Tags are key-value pairs that provide additional context, helping in categorizing and filtering metrics. This makes it easier to analyze and monitor specific aspects of your application. ### Tagging a Specific Metric You can tag individual metrics using the `Metric.tagged` function. This allows you to add specific tags to a single metric, providing detailed context without applying tags globally. **Example** (Tagging an Individual Metric) ```ts twoslash import { Metric } from "effect" // Create a counter metric for request count // and tag it with "environment: production" const counter = Metric.counter("request_count").pipe( Metric.tagged("environment", "production") ) ``` Here, the `request_count` metric is tagged with `"environment": "production"`, allowing you to filter or analyze metrics by this tag later. ### Tagging Multiple Metrics You can use `Effect.tagMetrics` to apply tags to all metrics within the same context. This is useful when you want to apply common tags, like the environment (e.g., "production" or "development"), across multiple metrics. **Example** (Tagging Multiple Metrics) ```ts twoslash import { Metric, Effect } from "effect" // Create two separate counters const counter1 = Metric.counter("counter1") const counter2 = Metric.counter("counter2") // Define a task that simulates some work with a slight delay const task = Effect.succeed(1).pipe(Effect.delay("100 millis")) // Apply the environment tag to both counters in the same context Effect.gen(function* () { yield* counter1(task) yield* counter2(task) }).pipe(Effect.tagMetrics("environment", "production")) ``` If you only want to apply tags within a specific [scope](/docs/resource-management/scope/), you can use `Effect.tagMetricsScoped`. This limits the tag application to metrics within that scope, allowing for more precise tagging control. # [Command](https://effect.website/docs/platform/command/) ## Overview The `@effect/platform/Command` module provides a way to create and run commands with the specified process name and an optional list of arguments. ## Creating Commands The `Command.make` function generates a command object, which includes details such as the process name, arguments, and environment. **Example** (Defining a Command for Directory Listing) ```ts twoslash import { Command } from "@effect/platform" const command = Command.make("ls", "-al") console.log(command) /* { _id: '@effect/platform/Command', _tag: 'StandardCommand', command: 'ls', args: [ '-al' ], env: {}, cwd: { _id: 'Option', _tag: 'None' }, shell: false, gid: { _id: 'Option', _tag: 'None' }, uid: { _id: 'Option', _tag: 'None' } } */ ``` This command object does not execute until run by an executor. ## Running Commands You need a `CommandExecutor` to run the command, which can capture output in various formats such as strings, lines, or streams. **Example** (Running a Command and Printing Output) ```ts twoslash import { Command } from "@effect/platform" import { NodeContext, NodeRuntime } from "@effect/platform-node" import { Effect } from "effect" const command = Command.make("ls", "-al") // The program depends on a CommandExecutor const program = Effect.gen(function* () { // Runs the command returning the output as a string const output = yield* Command.string(command) console.log(output) }) // Provide the necessary CommandExecutor NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer))) ``` ### Output Formats You can choose different methods to handle command output: | Method | Description | | ------------- | ---------------------------------------------------------------------------------------- | | `string` | Runs the command returning the output as a string (with the specified encoding) | | `lines` | Runs the command returning the output as an array of lines (with the specified encoding) | | `stream` | Runs the command returning the output as a stream of `Uint8Array` chunks | | `streamLines` | Runs the command returning the output as a stream of lines (with the specified encoding) | ### exitCode If you only need the exit code of a command, use `Command.exitCode`. **Example** (Getting the Exit Code) ```ts twoslash import { Command } from "@effect/platform" import { NodeContext, NodeRuntime } from "@effect/platform-node" import { Effect } from "effect" const command = Command.make("ls", "-al") const program = Effect.gen(function* () { const exitCode = yield* Command.exitCode(command) console.log(exitCode) }) NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer))) // Output: 0 ``` ## Custom Environment Variables You can customize environment variables in a command by using `Command.env`. This is useful when you need specific variables for the command's execution. **Example** (Setting Environment Variables) In this example, the command runs in a shell to ensure environment variables are correctly processed. ```ts twoslash import { Command } from "@effect/platform" import { NodeContext, NodeRuntime } from "@effect/platform-node" import { Effect } from "effect" const command = Command.make("echo", "-n", "$MY_CUSTOM_VAR").pipe( Command.env({ MY_CUSTOM_VAR: "Hello, this is a custom environment variable!" }), // Use shell to interpret variables correctly // on Windows and Unix-like systems Command.runInShell(true) ) const program = Effect.gen(function* () { const output = yield* Command.string(command) console.log(output) }) NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer))) // Output: Hello, this is a custom environment variable! ``` ## Feeding Input to a Command You can send input directly to a command's standard input using the `Command.feed` function. **Example** (Sending Input to a Command's Standard Input) ```ts twoslash import { Command } from "@effect/platform" import { NodeContext, NodeRuntime } from "@effect/platform-node" import { Effect } from "effect" const command = Command.make("cat").pipe(Command.feed("Hello")) const program = Effect.gen(function* () { console.log(yield* Command.string(command)) }) NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer))) // Output: Hello ``` ## Fetching Process Details You can access details about a running process, such as `exitCode`, `stdout`, and `stderr`. **Example** (Accessing Exit Code and Streams from a Running Process) ```ts twoslash import { Command } from "@effect/platform" import { NodeContext, NodeRuntime } from "@effect/platform-node" import { Effect, Stream, String, pipe } from "effect" // Helper function to collect stream output as a string const runString = ( stream: Stream.Stream ): Effect.Effect => stream.pipe( Stream.decodeText(), Stream.runFold(String.empty, String.concat) ) const program = Effect.gen(function* () { const command = Command.make("ls") const [exitCode, stdout, stderr] = yield* pipe( // Start running the command and return a handle to the running process Command.start(command), Effect.flatMap((process) => Effect.all( [ // Waits for the process to exit and returns // the ExitCode of the command that was run process.exitCode, // The standard output stream of the process runString(process.stdout), // The standard error stream of the process runString(process.stderr) ], { concurrency: 3 } ) ) ) console.log({ exitCode, stdout, stderr }) }) NodeRuntime.runMain( Effect.scoped(program).pipe(Effect.provide(NodeContext.layer)) ) ``` ## Streaming stdout to process.stdout To stream a command's `stdout` directly to `process.stdout`, you can use the following approach: **Example** (Streaming Command Output Directly to Standard Output) ```ts twoslash import { Command } from "@effect/platform" import { NodeContext, NodeRuntime } from "@effect/platform-node" import { Effect } from "effect" // Create a command to run `cat` on a file and inherit stdout const program = Command.make("cat", "./some-file.txt").pipe( Command.stdout("inherit"), // Stream stdout to process.stdout Command.exitCode // Get the exit code ) NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer))) ``` # [Tracing in Effect](https://effect.website/docs/observability/tracing/) ## Overview import { Tabs, TabItem, Steps, Aside } from "@astrojs/starlight/components" Although logs and metrics are useful to understand the behavior of individual services, they are not enough to provide a complete overview of the lifetime of a request in a distributed system. In a distributed system, a request can span multiple services and each service can make multiple requests to other services to fulfill the request. In such a scenario, we need to have a way to track the lifetime of a request across multiple services to diagnose what services are the bottlenecks and where the request is spending most of its time. ## Spans A **span** represents a single unit of work or operation within a request. It provides a detailed view of what happened during the execution of that specific operation. Each span typically contains the following information: | Span Component | Description | | ---------------- | ------------------------------------------------------------------ | | **Name** | Describes the specific operation being tracked. | | **Timing Data** | Timestamps indicating when the operation started and its duration. | | **Log Messages** | Structured logs capturing important events during the operation. | | **Attributes** | Metadata providing additional context about the operation. | Spans are key building blocks in tracing, helping you visualize and understand the flow of requests through various services. ## Traces A trace records the paths taken by requests (made by an application or end-user) as they propagate through multi-service architectures, like microservice and serverless applications. Without tracing, it is challenging to pinpoint the cause of performance problems in a distributed system. A trace is made of one or more spans. The first span represents the root span. Each root span represents a request from start to finish. The spans underneath the parent provide a more in-depth context of what occurs during a request (or what steps make up a request). Many Observability back-ends visualize traces as waterfall diagrams that may look something like this: ![Trace Waterfall Diagram](../_assets/waterfall-trace.svg "An image displaying an application trace visualized as a waterfall diagram") Waterfall diagrams show the parent-child relationship between a root span and its child spans. When a span encapsulates another span, this also represents a nested relationship. ## Creating Spans You can add tracing to an effect by creating a span using the `Effect.withSpan` API. This helps you track specific operations within the effect. **Example** (Adding a Span to an Effect) ```ts twoslash import { Effect } from "effect" // Define an effect that delays for 100 milliseconds const program = Effect.void.pipe(Effect.delay("100 millis")) // Instrument the effect with a span for tracing const instrumented = program.pipe(Effect.withSpan("myspan")) ``` Instrumenting an effect with a span does not change its type. If you start with an `Effect`, the result remains an `Effect`. ## Printing Spans To print spans for debugging or analysis, you'll need to install the required tracing tools. Here’s how to set them up for your project. ### Installing Dependencies Choose your package manager and install the necessary libraries: ```sh showLineNumbers=false # Install the main library for integrating OpenTelemetry with Effect npm install @effect/opentelemetry # Install the required OpenTelemetry SDKs for tracing and metrics npm install @opentelemetry/sdk-trace-base npm install @opentelemetry/sdk-trace-node npm install @opentelemetry/sdk-trace-web npm install @opentelemetry/sdk-metrics ``` ```sh showLineNumbers=false # Install the main library for integrating OpenTelemetry with Effect pnpm add @effect/opentelemetry # Install the required OpenTelemetry SDKs for tracing and metrics pnpm add @opentelemetry/sdk-trace-base pnpm add @opentelemetry/sdk-trace-node pnpm add @opentelemetry/sdk-trace-web pnpm add @opentelemetry/sdk-metrics ``` ```sh showLineNumbers=false # Install the main library for integrating OpenTelemetry with Effect yarn add @effect/opentelemetry # Install the required OpenTelemetry SDKs for tracing and metrics yarn add @opentelemetry/sdk-trace-base yarn add @opentelemetry/sdk-trace-node yarn add @opentelemetry/sdk-trace-web yarn add @opentelemetry/sdk-metrics ``` ```sh showLineNumbers=false # Install the main library for integrating OpenTelemetry with Effect bun add @effect/opentelemetry # Install the required OpenTelemetry SDKs for tracing and metrics bun add @opentelemetry/sdk-trace-base bun add @opentelemetry/sdk-trace-node bun add @opentelemetry/sdk-trace-web bun add @opentelemetry/sdk-metrics ``` ### Printing a Span to the Console Once the dependencies are installed, you can set up span printing using OpenTelemetry. Here's an example showing how to print a span for an effect. **Example** (Setting Up and Printing a Span) ```ts twoslash import { Effect } from "effect" import { NodeSdk } from "@effect/opentelemetry" import { ConsoleSpanExporter, BatchSpanProcessor } from "@opentelemetry/sdk-trace-base" // Define an effect that delays for 100 milliseconds const program = Effect.void.pipe(Effect.delay("100 millis")) // Instrument the effect with a span for tracing const instrumented = program.pipe(Effect.withSpan("myspan")) // Set up tracing with the OpenTelemetry SDK const NodeSdkLive = NodeSdk.layer(() => ({ resource: { serviceName: "example" }, // Export span data to the console spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter()) })) // Run the effect, providing the tracing layer Effect.runPromise(instrumented.pipe(Effect.provide(NodeSdkLive))) /* Example Output: { resource: { attributes: { 'service.name': 'example', 'telemetry.sdk.language': 'nodejs', 'telemetry.sdk.name': '@effect/opentelemetry', 'telemetry.sdk.version': '1.28.0' } }, instrumentationScope: { name: 'example', version: undefined, schemaUrl: undefined }, traceId: '673c06608bd815f7a75bf897ef87e186', parentId: undefined, traceState: undefined, name: 'myspan', id: '401b2846170cd17b', kind: 0, timestamp: 1733220735529855.5, duration: 102079.958, attributes: {}, status: { code: 1 }, events: [], links: [] } */ ``` ### Understanding the Span Output The output provides detailed information about the span: | Field | Description | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `traceId` | A unique identifier for the entire trace, helping trace requests or operations as they move through an application. | | `parentId` | Identifies the parent span of the current span, marked as `undefined` in the output when there is no parent span, making it a root span. | | `name` | Describes the name of the span, indicating the operation being tracked (e.g., "myspan"). | | `id` | A unique identifier for the current span, distinguishing it from other spans within a trace. | | `timestamp` | A timestamp representing when the span started, measured in microseconds since the Unix epoch. | | `duration` | Specifies the duration of the span, representing the time taken to complete the operation (e.g., `2895.769` microseconds). | | `attributes` | Spans may contain attributes, which are key-value pairs providing additional context or information about the operation. In this output, it's an empty object, indicating no specific attributes in this span. | | `status` | The status field provides information about the span's status. In this case, it has a code of 1, which typically indicates an OK status (whereas a code of 2 signifies an ERROR status) | | `events` | Spans can include events, which are records of specific moments during the span's lifecycle. In this output, it's an empty array, suggesting no specific events recorded. | | `links` | Links can be used to associate this span with other spans in different traces. In the output, it's an empty array, indicating no specific links for this span. | ### Span Capturing an Error Here's how a span looks when the effect encounters an error: **Example** (Span for an Effect that Fails) ```ts twoslash "code: 2" import { Effect } from "effect" import { NodeSdk } from "@effect/opentelemetry" import { ConsoleSpanExporter, BatchSpanProcessor } from "@opentelemetry/sdk-trace-base" const program = Effect.fail("Oh no!").pipe( Effect.delay("100 millis"), Effect.withSpan("myspan") ) const NodeSdkLive = NodeSdk.layer(() => ({ resource: { serviceName: "example" }, spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter()) })) Effect.runPromiseExit(program.pipe(Effect.provide(NodeSdkLive))).then( console.log ) /* Example Output: { resource: { attributes: { 'service.name': 'example', 'telemetry.sdk.language': 'nodejs', 'telemetry.sdk.name': '@effect/opentelemetry', 'telemetry.sdk.version': '1.28.0' } }, instrumentationScope: { name: 'example', version: undefined, schemaUrl: undefined }, traceId: 'eee9619866179f209b7aae277283e71f', parentId: undefined, traceState: undefined, name: 'myspan', id: '3a5725c91884c9e1', kind: 0, timestamp: 1733220830575626, duration: 106578.042, attributes: { 'code.stacktrace': 'at (/Users/giuliocanti/Documents/GitHub/website/content/dev/index.ts:10:10)' }, status: { code: 2, message: 'Oh no!' }, events: [ { name: 'exception', attributes: { 'exception.type': 'Error', 'exception.message': 'Oh no!', 'exception.stacktrace': 'Error: Oh no!' }, time: [ 1733220830, 682204083 ], droppedAttributesCount: 0 } ], links: [] } { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Oh no!' } } */ ``` In this example, the span's status code is `2`, indicating an error. The message in the status provides more details about the failure. ## Adding Annotations You can provide extra information to a span by utilizing the `Effect.annotateCurrentSpan` function. This function allows you to attach key-value pairs, offering more context about the execution of the span. **Example** (Annotating a Span) ```ts twoslash "attributes: { key: 'value' }" import { Effect } from "effect" import { NodeSdk } from "@effect/opentelemetry" import { ConsoleSpanExporter, BatchSpanProcessor } from "@opentelemetry/sdk-trace-base" const program = Effect.void.pipe( Effect.delay("100 millis"), // Annotate the span with a key-value pair Effect.tap(() => Effect.annotateCurrentSpan("key", "value")), // Wrap the effect in a span named 'myspan' Effect.withSpan("myspan") ) // Set up tracing with the OpenTelemetry SDK const NodeSdkLive = NodeSdk.layer(() => ({ resource: { serviceName: "example" }, spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter()) })) // Run the effect, providing the tracing layer Effect.runPromise(program.pipe(Effect.provide(NodeSdkLive))) /* Example Output: { resource: { attributes: { 'service.name': 'example', 'telemetry.sdk.language': 'nodejs', 'telemetry.sdk.name': '@effect/opentelemetry', 'telemetry.sdk.version': '1.28.0' } }, instrumentationScope: { name: 'example', version: undefined, schemaUrl: undefined }, traceId: 'c8120e01c0f1ea83ccc1d388e5cdebd3', parentId: undefined, traceState: undefined, name: 'myspan', id: '81c430ba4979f1db', kind: 0, timestamp: 1733220874356084, duration: 102821.417, attributes: { key: 'value' }, status: { code: 1 }, events: [], links: [] } */ ``` ## Logs as events In the context of tracing, logs are converted into "Span Events." These events offer structured insights into your application's activities and provide a timeline of when specific operations occurred. ```ts twoslash {47} import { Effect } from "effect" import { NodeSdk } from "@effect/opentelemetry" import { ConsoleSpanExporter, BatchSpanProcessor } from "@opentelemetry/sdk-trace-base" // Define a program that logs a message and delays for 100 milliseconds const program = Effect.log("Hello").pipe( Effect.delay("100 millis"), Effect.withSpan("myspan") ) // Set up tracing with the OpenTelemetry SDK const NodeSdkLive = NodeSdk.layer(() => ({ resource: { serviceName: "example" }, spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter()) })) // Run the effect, providing the tracing layer Effect.runPromise(program.pipe(Effect.provide(NodeSdkLive))) /* Example Output: { resource: { attributes: { 'service.name': 'example', 'telemetry.sdk.language': 'nodejs', 'telemetry.sdk.name': '@effect/opentelemetry', 'telemetry.sdk.version': '1.28.0' } }, instrumentationScope: { name: 'example', version: undefined, schemaUrl: undefined }, traceId: 'b0f4f012b5b13c0a040f7002a1d7b020', parentId: undefined, traceState: undefined, name: 'myspan', id: 'b9ba8472002715a8', kind: 0, timestamp: 1733220905504162.2, duration: 103790, attributes: {}, status: { code: 1 }, events: [ { name: 'Hello', attributes: { 'effect.fiberId': '#0', 'effect.logLevel': 'INFO' }, // Log attributes time: [ 1733220905, 607761042 ], // Event timestamp droppedAttributesCount: 0 } ], links: [] } */ ``` Each span can include events, which capture specific moments during the execution of a span. In this example, a log message `"Hello"` is recorded as an event within the span. Key details of the event include: | Field | Description | | ------------------------ | ------------------------------------------------------------------------------------------------- | | `name` | The name of the event, which corresponds to the logged message (e.g., `'Hello'`). | | `attributes` | Key-value pairs that provide additional context about the event, such as `fiberId` and log level. | | `time` | The timestamp of when the event occurred, shown in a high-precision format. | | `droppedAttributesCount` | Indicates how many attributes were discarded, if any. In this case, no attributes were dropped. | ## Nesting Spans Spans can be nested to represent a hierarchy of operations. This allows you to track how different parts of your application relate to one another during execution. The following example demonstrates how to create and manage nested spans. **Example** (Nesting Spans in a Trace) ```ts twoslash "a09e5c3fdfdbbc1d" import { Effect } from "effect" import { NodeSdk } from "@effect/opentelemetry" import { ConsoleSpanExporter, BatchSpanProcessor } from "@opentelemetry/sdk-trace-base" const child = Effect.void.pipe( Effect.delay("100 millis"), Effect.withSpan("child") ) const parent = Effect.gen(function* () { yield* Effect.sleep("20 millis") yield* child yield* Effect.sleep("10 millis") }).pipe(Effect.withSpan("parent")) // Set up tracing with the OpenTelemetry SDK const NodeSdkLive = NodeSdk.layer(() => ({ resource: { serviceName: "example" }, spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter()) })) // Run the effect, providing the tracing layer Effect.runPromise(parent.pipe(Effect.provide(NodeSdkLive))) /* Example Output: { resource: { attributes: { 'service.name': 'example', 'telemetry.sdk.language': 'nodejs', 'telemetry.sdk.name': '@effect/opentelemetry', 'telemetry.sdk.version': '1.28.0' } }, instrumentationScope: { name: 'example', version: undefined, schemaUrl: undefined }, traceId: 'a9cd69ad70698a0c7b7b774597c77d39', parentId: 'a09e5c3fdfdbbc1d', // This indicates the span is a child of 'parent' traceState: undefined, name: 'child', id: '210d2f9b648389a4', // Unique ID for the child span kind: 0, timestamp: 1733220970590126.2, duration: 101579.875, attributes: {}, status: { code: 1 }, events: [], links: [] } { resource: { attributes: { 'service.name': 'example', 'telemetry.sdk.language': 'nodejs', 'telemetry.sdk.name': '@effect/opentelemetry', 'telemetry.sdk.version': '1.28.0' } }, instrumentationScope: { name: 'example', version: undefined, schemaUrl: undefined }, traceId: 'a9cd69ad70698a0c7b7b774597c77d39', parentId: undefined, // Indicates this is the root span traceState: undefined, name: 'parent', id: 'a09e5c3fdfdbbc1d', // Unique ID for the parent span kind: 0, timestamp: 1733220970569015.2, duration: 132612.208, attributes: {}, status: { code: 1 }, events: [], links: [] } */ ``` The parent-child relationship is evident in the span output, where the `parentId` of the `child` span matches the `id` of the `parent` span. This structure helps track how operations are related within a single trace. ## Tutorial: Visualizing Traces with Docker, Prometheus, Grafana, and Tempo In this tutorial, we'll guide you through simulating and visualizing traces using a sample instrumented Node.js application. We will use Docker, Prometheus, Grafana, and Tempo to create, collect, and visualize traces. ### Tools Explained Let's understand the tools we'll be using in simple terms: - **Docker**: Docker allows us to run applications in containers. Think of a container as a lightweight and isolated environment where your application can run consistently, regardless of the host system. It's a bit like a virtual machine but more efficient. - **Prometheus**: Prometheus is a monitoring and alerting toolkit. It collects metrics and data about your applications and stores them for further analysis. This helps in identifying performance issues and understanding the behavior of your applications. - **Grafana**: Grafana is a visualization and analytics platform. It helps in creating beautiful and interactive dashboards to visualize your application's data. You can use it to graphically represent metrics collected by Prometheus. - **Tempo**: Tempo is a distributed tracing system that allows you to trace the journey of a request as it flows through your application. It provides insights into how requests are processed and helps in debugging and optimizing your applications. ### Getting Docker To get Docker, follow these steps: 1. Visit the Docker website at [https://www.docker.com/](https://www.docker.com/). 2. Download Docker Desktop for your operating system (Windows or macOS) and install it. 3. After installation, open Docker Desktop, and it will run in the background. ### Simulating Traces Now, let's simulate traces using a sample Node.js application. We'll provide you with the code and guide you on setting up the necessary components. 1. **Download Docker Files**. Download the required Docker files: [docker.zip](/tracing/docker.zip) 2. **Set Up docker**. Unzip the downloaded file, navigate to the `/docker/local` directory in your terminal or command prompt and run the following command to start the necessary services: ```sh showLineNumbers=false docker-compose up ``` 3. **Simulate Traces**. Run the following example code in your Node.js environment. This code simulates a set of tasks and generates traces. Before proceeding, you'll need to install additional libraries in addition to the latest version of `effect`. Here are the required libraries: ```sh showLineNumbers=false npm install @effect/opentelemetry npm install @opentelemetry/exporter-trace-otlp-http npm install @opentelemetry/sdk-trace-base npm install @opentelemetry/sdk-trace-web npm install @opentelemetry/sdk-trace-node ``` ```sh showLineNumbers=false pnpm add @effect/opentelemetry pnpm add @opentelemetry/exporter-trace-otlp-http pnpm add @opentelemetry/sdk-trace-base # For NodeJS applications pnpm add @opentelemetry/sdk-trace-node # For browser applications pnpm add @opentelemetry/sdk-trace-web # If you also need to export metrics pnpm add @opentelemetry/sdk-metrics ``` ```sh showLineNumbers=false yarn add @effect/opentelemetry yarn add @opentelemetry/exporter-trace-otlp-http yarn add @opentelemetry/sdk-trace-base # For NodeJS applications yarn add @opentelemetry/sdk-trace-node # For browser applications yarn add @opentelemetry/sdk-trace-web # If you also need to export metrics yarn add @opentelemetry/sdk-metrics ``` ```sh showLineNumbers=false bun add @effect/opentelemetry bun add @opentelemetry/exporter-trace-otlp-http bun add @opentelemetry/sdk-trace-base # For NodeJS applications bun add @opentelemetry/sdk-trace-node # For browser applications bun add @opentelemetry/sdk-trace-web # If you also need to export metrics bun add @opentelemetry/sdk-metrics ``` ```ts twoslash import { Effect } from "effect" import { NodeSdk } from "@effect/opentelemetry" import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base" import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http" // Function to simulate a task with possible subtasks const task = ( name: string, delay: number, children: ReadonlyArray> = [] ) => Effect.gen(function* () { yield* Effect.log(name) yield* Effect.sleep(`${delay} millis`) for (const child of children) { yield* child } yield* Effect.sleep(`${delay} millis`) }).pipe(Effect.withSpan(name)) const poll = task("/poll", 1) // Create a program with tasks and subtasks const program = task("client", 2, [ task("/api", 3, [ task("/authN", 4, [task("/authZ", 5)]), task("/payment Gateway", 6, [ task("DB", 7), task("Ext. Merchant", 8) ]), task("/dispatch", 9, [ task("/dispatch/search", 10), Effect.all([poll, poll, poll], { concurrency: "inherit" }), task("/pollDriver/{id}", 11) ]) ]) ]) const NodeSdkLive = NodeSdk.layer(() => ({ resource: { serviceName: "example" }, spanProcessor: new BatchSpanProcessor(new OTLPTraceExporter()) })) Effect.runPromise( program.pipe( Effect.provide(NodeSdkLive), Effect.catchAllCause(Effect.logError) ) ) /* Output: timestamp=... level=INFO fiber=#0 message=client timestamp=... level=INFO fiber=#0 message=/api timestamp=... level=INFO fiber=#0 message=/authN timestamp=... level=INFO fiber=#0 message=/authZ timestamp=... level=INFO fiber=#0 message="/payment Gateway" timestamp=... level=INFO fiber=#0 message=DB timestamp=... level=INFO fiber=#0 message="Ext. Merchant" timestamp=... level=INFO fiber=#0 message=/dispatch timestamp=... level=INFO fiber=#0 message=/dispatch/search timestamp=... level=INFO fiber=#3 message=/poll timestamp=... level=INFO fiber=#4 message=/poll timestamp=... level=INFO fiber=#5 message=/poll timestamp=... level=INFO fiber=#0 message=/pollDriver/{id} */ ``` 4. **Visualize Traces**. Now, open your web browser and go to `http://localhost:3000/explore`. You will see a generated `Trace ID` on the web page. Click on it to see the details of the trace. ![Traces in Grafana Tempo](../_assets/trace.png "An image displaying an application trace visualized as a waterfall diagram in Grafana Tempo") ## Integrations ### Sentry To send span data directly to Sentry for analysis, replace the default span processor with Sentry's implementation. This allows you to use Sentry as a backend for tracing and debugging. **Example** (Configuring Sentry for Tracing) ```ts twoslash import { NodeSdk } from "@effect/opentelemetry" import { SentrySpanProcessor } from "@sentry/opentelemetry" const NodeSdkLive = NodeSdk.layer(() => ({ resource: { serviceName: "example" }, spanProcessor: new SentrySpanProcessor() })) ``` # [FileSystem](https://effect.website/docs/platform/file-system/) ## Overview The `@effect/platform/FileSystem` module provides a set of operations for reading and writing from/to the file system. ## Basic Usage The module provides a single `FileSystem` [tag](/docs/requirements-management/services/), which acts as the gateway for interacting with the filesystem. **Example** (Accessing File System Operations) ```ts twoslash import { FileSystem } from "@effect/platform" import { Effect } from "effect" const program = Effect.gen(function* () { const fs = yield* FileSystem.FileSystem // Use `fs` to perform file system operations }) ``` The `FileSystem` interface includes the following operations: | Operation | Description | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **access** | Check if a file can be accessed. You can optionally specify the level of access to check for. | | **copy** | Copy a file or directory from `fromPath` to `toPath`. Equivalent to `cp -r`. | | **copyFile** | Copy a file from `fromPath` to `toPath`. | | **chmod** | Change the permissions of a file. | | **chown** | Change the owner and group of a file. | | **exists** | Check if a path exists. | | **link** | Create a hard link from `fromPath` to `toPath`. | | **makeDirectory** | Create a directory at `path`. You can optionally specify the mode and whether to recursively create nested directories. | | **makeTempDirectory** | Create a temporary directory. By default, the directory will be created inside the system's default temporary directory. | | **makeTempDirectoryScoped** | Create a temporary directory inside a scope. Functionally equivalent to `makeTempDirectory`, but the directory will be automatically deleted when the scope is closed. | | **makeTempFile** | Create a temporary file. The directory creation is functionally equivalent to `makeTempDirectory`. The file name will be a randomly generated string. | | **makeTempFileScoped** | Create a temporary file inside a scope. Functionally equivalent to `makeTempFile`, but the file will be automatically deleted when the scope is closed. | | **open** | Open a file at `path` with the specified `options`. The file handle will be automatically closed when the scope is closed. | | **readDirectory** | List the contents of a directory. You can recursively list the contents of nested directories by setting the `recursive` option. | | **readFile** | Read the contents of a file. | | **readFileString** | Read the contents of a file as a string. | | **readLink** | Read the destination of a symbolic link. | | **realPath** | Resolve a path to its canonicalized absolute pathname. | | **remove** | Remove a file or directory. By setting the `recursive` option to `true`, you can recursively remove nested directories. | | **rename** | Rename a file or directory. | | **sink** | Create a writable `Sink` for the specified `path`. | | **stat** | Get information about a file at `path`. | | **stream** | Create a readable `Stream` for the specified `path`. | | **symlink** | Create a symbolic link from `fromPath` to `toPath`. | | **truncate** | Truncate a file to a specified length. If the `length` is not specified, the file will be truncated to length `0`. | | **utimes** | Change the file system timestamps of the file at `path`. | | **watch** | Watch a directory or file for changes. | | **writeFile** | Write data to a file at `path`. | | **writeFileString** | Write a string to a file at `path`. | **Example** (Reading a File as a String) ```ts twoslash import { FileSystem } from "@effect/platform" import { NodeContext, NodeRuntime } from "@effect/platform-node" import { Effect } from "effect" // ┌─── Effect // ▼ const program = Effect.gen(function* () { const fs = yield* FileSystem.FileSystem // Reading the content of the same file where this code is written const content = yield* fs.readFileString("./index.ts", "utf8") console.log(content) }) // Provide the necessary context and run the program NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer))) ``` ## Mocking the File System In testing environments, you may want to mock the file system to avoid performing actual disk operations. The `FileSystem.layerNoop` provides a no-operation implementation of the `FileSystem` service. Most operations in `FileSystem.layerNoop` return a **failure** (e.g., `Effect.fail` for missing files) or a **defect** (e.g., `Effect.die` for unimplemented features). However, you can override specific behaviors by passing an object to `FileSystem.layerNoop` to define custom return values for selected methods. **Example** (Mocking File System with Custom Behavior) ```ts twoslash import { FileSystem } from "@effect/platform" import { Effect } from "effect" const program = Effect.gen(function* () { const fs = yield* FileSystem.FileSystem const exists = yield* fs.exists("/some/path") console.log(exists) const content = yield* fs.readFileString("/some/path") console.log(content) }) // ┌─── Layer // ▼ const customMock = FileSystem.layerNoop({ readFileString: () => Effect.succeed("mocked content"), exists: (path) => Effect.succeed(path === "/some/path") }) // Provide the customized FileSystem mock implementation Effect.runPromise(program.pipe(Effect.provide(customMock))) /* Output: true mocked content */ ``` # [Introduction to Effect Platform](https://effect.website/docs/platform/introduction/) ## Overview import { Aside, Tabs, TabItem, Badge } from "@astrojs/starlight/components" `@effect/platform` is a library for building platform-independent abstractions in environments such as Node.js, Deno, Bun, and browsers. With `@effect/platform`, you can integrate abstract services like [FileSystem](/docs/platform/file-system/) or [Terminal](/docs/platform/terminal/) into your program. When assembling your final application, you can provide specific [layers](/docs/requirements-management/layers/) for the target platform using the corresponding packages: - `@effect/platform-node` for Node.js or Deno - `@effect/platform-bun` for Bun - `@effect/platform-browser` for browsers ### Stable Modules The following modules are stable and their documentation is available on this website: | Module | Description | Status | | ------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------- | | [Command](/docs/platform/command/) | Provides a way to interact with the command line. | | | [FileSystem](/docs/platform/file-system/) | A module for file system operations. | | | [KeyValueStore](/docs/platform/key-value-store/) | Manages key-value pairs for data storage. | | | [Path](/docs/platform/path/) | Utilities for working with file paths. | | | [PlatformLogger](/docs/platform/platformlogger/) | Log messages to a file using the FileSystem APIs. | | | [Runtime](/docs/platform/runtime/) | Run your program with built-in error handling and logging. | | | [Terminal](/docs/platform/terminal/) | Tools for terminal interaction. | | ### Unstable Modules Some modules in `@effect/platform` are still in development or marked as experimental. These features are subject to change. | Module | Description | Status | | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------- | | [Http API](https://github.com/Effect-TS/effect/blob/main/packages/platform/README.md#http-api) | Provide a declarative way to define HTTP APIs. | | | [Http Client](https://github.com/Effect-TS/effect/blob/main/packages/platform/README.md#http-client) | A client for making HTTP requests. | | | [Http Server](https://github.com/Effect-TS/effect/blob/main/packages/platform/README.md#http-server) | A server for handling HTTP requests. | | | [Socket](https://effect-ts.github.io/effect/platform/Socket.ts.html) | A module for socket-based communication. | | | [Worker](https://effect-ts.github.io/effect/platform/Worker.ts.html) | A module for running tasks in separate workers. | | For the most up-to-date documentation and details, please refer to the official [README](https://github.com/Effect-TS/effect/blob/main/packages/platform/README.md) of the package. ## Installation To install the **beta** version: ```sh showLineNumbers=false npm install @effect/platform ``` ```sh showLineNumbers=false pnpm add @effect/platform ``` ```sh showLineNumbers=false yarn add @effect/platform ``` ```sh showLineNumbers=false bun add @effect/platform ``` ```sh showLineNumbers=false deno add npm:@effect/platform ``` ## Getting Started with Cross-Platform Programming Here's a basic example using the `Path` module to create a file path, which can run across different environments: **Example** (Cross-Platform Path Handling) ```ts twoslash title="index.ts" import { Path } from "@effect/platform" import { Effect } from "effect" const program = Effect.gen(function* () { // Access the Path service const path = yield* Path.Path // Join parts of a path to create a complete file path const mypath = path.join("tmp", "file.txt") console.log(mypath) }) ``` ### Running the Program in Node.js or Deno First, install the Node.js-specific package: ```sh showLineNumbers=false npm install @effect/platform-node ``` ```sh showLineNumbers=false pnpm add @effect/platform-node ``` ```sh showLineNumbers=false yarn add @effect/platform-node ``` ```sh showLineNumbers=false deno add npm:@effect/platform-node ``` Update the program to load the Node.js-specific context: **Example** (Providing Node.js Context) ```ts twoslash title="index.ts" ins={3,15} import { Path } from "@effect/platform" import { Effect } from "effect" import { NodeContext, NodeRuntime } from "@effect/platform-node" const program = Effect.gen(function* () { // Access the Path service const path = yield* Path.Path // Join parts of a path to create a complete file path const mypath = path.join("tmp", "file.txt") console.log(mypath) }) NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer))) ``` Finally, run the program in Node.js using `tsx`, or directly in Deno: ```sh showLineNumbers=false npx tsx index.ts # Output: tmp/file.txt ``` ```sh showLineNumbers=false pnpm dlx tsx index.ts # Output: tmp/file.txt ``` ```sh showLineNumbers=false yarn dlx tsx index.ts # Output: tmp/file.txt ``` ```sh showLineNumbers=false deno run index.ts # Output: tmp/file.txt # or deno run -RE index.ts # Output: tmp/file.txt # (granting required Read and Environment permissions without being prompted) ``` ### Running the Program in Bun To run the same program in Bun, first install the Bun-specific package: ```sh showLineNumbers=false bun add @effect/platform-bun ``` Update the program to use the Bun-specific context: **Example** (Providing Bun Context) ```ts twoslash title="index.ts" ins={3,15} import { Path } from "@effect/platform" import { Effect } from "effect" import { BunContext, BunRuntime } from "@effect/platform-bun" const program = Effect.gen(function* () { // Access the Path service const path = yield* Path.Path // Join parts of a path to create a complete file path const mypath = path.join("tmp", "file.txt") console.log(mypath) }) BunRuntime.runMain(program.pipe(Effect.provide(BunContext.layer))) ``` Run the program in Bun: ```sh showLineNumbers=false bun index.ts tmp/file.txt ``` # [KeyValueStore](https://effect.website/docs/platform/key-value-store/) ## Overview The `@effect/platform/KeyValueStore` module provides a robust and effectful interface for managing key-value pairs. It supports asynchronous operations, ensuring data integrity and consistency, and includes built-in implementations for in-memory, file system-based, and schema-validated stores. ## Basic Usage The module provides a single `KeyValueStore` [tag](/docs/requirements-management/services/), which acts as the gateway for interacting with the store. **Example** (Accessing the KeyValueStore Service) ```ts twoslash import { KeyValueStore } from "@effect/platform" import { Effect } from "effect" const program = Effect.gen(function* () { const kv = yield* KeyValueStore.KeyValueStore // Use `kv` to perform operations on the store }) ``` The `KeyValueStore` interface includes the following operations: | Operation | Description | | -------------------- | -------------------------------------------------------------------- | | **get** | Returns the value as `string` of the specified key if it exists. | | **getUint8Array** | Returns the value as `Uint8Array` of the specified key if it exists. | | **set** | Sets the value of the specified key. | | **remove** | Removes the specified key. | | **clear** | Removes all entries. | | **size** | Returns the number of entries. | | **modify** | Updates the value of the specified key if it exists. | | **modifyUint8Array** | Updates the value of the specified key if it exists. | | **has** | Check if a key exists. | | **isEmpty** | Check if the store is empty. | | **forSchema** | Create a [SchemaStore](#schemastore) for the specified schema. | **Example** (Working with Key-Value Pairs) ```ts twoslash import { KeyValueStore, layerMemory } from "@effect/platform/KeyValueStore" import { Effect } from "effect" const program = Effect.gen(function* () { const kv = yield* KeyValueStore // Initially, the store is empty console.log(yield* kv.size) // Set a key-value pair yield* kv.set("key", "value") console.log(yield* kv.size) // Retrieve the value for the specified key const value = yield* kv.get("key") console.log(value) // Remove the key-value pair yield* kv.remove("key") console.log(yield* kv.size) }) // Provide an in-memory KeyValueStore implementation Effect.runPromise(program.pipe(Effect.provide(layerMemory))) /* Output: 0 1 { _id: 'Option', _tag: 'Some', value: 'value' } 0 */ ``` ## Built-in Implementations The module provides several built-in implementations of the `KeyValueStore` interface, available as [layers](/docs/requirements-management/layers/), to suit different needs: | Implementation | Description | | --------------------- | --------------------------------------------------------------------------------------------------------- | | **In-Memory Store** | `layerMemory` provides a simple, in-memory key-value store, ideal for lightweight or testing scenarios. | | **File System Store** | `layerFileSystem` offers a file-based store for persistent storage needs. | | **Schema Store** | `layerSchema` enables schema-based validation for stored values, ensuring data integrity and type safety. | ## SchemaStore The `SchemaStore` interface allows you to validate and parse values according to a defined [schema](/docs/schema/introduction/). This ensures that all data stored in the key-value store adheres to the specified structure, enhancing data integrity and type safety. **Example** (Using Schema Validation in KeyValueStore) ```ts twoslash import { KeyValueStore, layerMemory } from "@effect/platform/KeyValueStore" import { Effect, Schema } from "effect" // Define a schema for the values const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) const program = Effect.gen(function* () { // Create a SchemaStore based on the Person schema const kv = (yield* KeyValueStore).forSchema(Person) // Add a value that adheres to the schema const value = { name: "Alice", age: 30 } yield* kv.set("user1", value) console.log(yield* kv.size) // Retrieve and log the value console.log(yield* kv.get("user1")) }) // Use the in-memory store implementation Effect.runPromise(program.pipe(Effect.provide(layerMemory))) /* Output: 1 { _id: 'Option', _tag: 'Some', value: { name: 'Alice', age: 30 } } */ ``` # [Path](https://effect.website/docs/platform/path/) ## Overview The `@effect/platform/Path` module provides a set of operations for working with file paths. ## Basic Usage The module provides a single `Path` [tag](/docs/requirements-management/services/), which acts as the gateway for interacting with paths. **Example** (Accessing the Path Service) ```ts twoslash import { Path } from "@effect/platform" import { Effect } from "effect" const program = Effect.gen(function* () { const path = yield* Path.Path // Use `path` to perform various path operations }) ``` The `Path` interface includes the following operations: | Operation | Description | | -------------------- | -------------------------------------------------------------------------- | | **basename** | Returns the last part of a path, optionally removing a given suffix. | | **dirname** | Returns the directory part of a path. | | **extname** | Returns the file extension from a path. | | **format** | Formats a path object into a path string. | | **fromFileUrl** | Converts a file URL to a path. | | **isAbsolute** | Checks if a path is absolute. | | **join** | Joins multiple path segments into one. | | **normalize** | Normalizes a path by resolving `.` and `..` segments. | | **parse** | Parses a path string into an object with its segments. | | **relative** | Computes the relative path from one path to another. | | **resolve** | Resolves a sequence of paths to an absolute path. | | **sep** | Returns the platform-specific path segment separator (e.g., `/` on POSIX). | | **toFileUrl** | Converts a path to a file URL. | | **toNamespacedPath** | Converts a path to a namespaced path (specific to Windows). | **Example** (Joining Path Segments) ```ts twoslash import { Path } from "@effect/platform" import { Effect } from "effect" import { NodeContext, NodeRuntime } from "@effect/platform-node" const program = Effect.gen(function* () { const path = yield* Path.Path const mypath = path.join("tmp", "file.txt") console.log(mypath) }) NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer))) // Output: "tmp/file.txt" ``` # [PlatformLogger](https://effect.website/docs/platform/platformlogger/) ## Overview Effect's logging system generally writes messages to the console by default. However, you might prefer to store logs in a file for easier debugging or archiving. The `PlatformLogger.toFile` function creates a logger that sends log messages to a file on disk. ### toFile Creates a new logger from an existing string-based logger, writing its output to the specified file. If you include a `batchWindow` duration when calling `toFile`, logs are batched for that period before being written. This can reduce overhead if your application produces many log entries. Without a `batchWindow`, logs are written as they arrive. Note that `toFile` returns an `Effect` that may fail with a `PlatformError` if the file cannot be opened or written to. Be sure to handle this possibility if you need to react to file I/O issues. **Example** (Directing Logs to a File) This logger requires a `FileSystem` implementation to open and write to the file. For Node.js, you can use `NodeFileSystem.layer`. ```ts twoslash import { PlatformLogger } from "@effect/platform" import { NodeFileSystem } from "@effect/platform-node" import { Effect, Layer, Logger } from "effect" // Create a string-based logger (logfmtLogger in this case) const myStringLogger = Logger.logfmtLogger // Apply toFile to write logs to "/tmp/log.txt" const fileLogger = myStringLogger.pipe( PlatformLogger.toFile("/tmp/log.txt") ) // Replace the default logger, providing NodeFileSystem // to access the file system const LoggerLive = Logger.replaceScoped( Logger.defaultLogger, fileLogger ).pipe(Layer.provide(NodeFileSystem.layer)) const program = Effect.log("Hello") // Run the program, writing logs to /tmp/log.txt Effect.runFork(program.pipe(Effect.provide(LoggerLive))) /* Logs will be written to "/tmp/log.txt" in the logfmt format, and won't appear on the console. */ ``` In the following example, logs are written to both the console and a file. The console uses the pretty logger, while the file uses the logfmt format. **Example** (Directing Logs to Both a File and the Console) ```ts twoslash import { PlatformLogger } from "@effect/platform" import { NodeFileSystem } from "@effect/platform-node" import { Effect, Layer, Logger } from "effect" const fileLogger = Logger.logfmtLogger.pipe( PlatformLogger.toFile("/tmp/log.txt") ) // Combine the pretty logger for console output with the file logger const bothLoggers = Effect.map(fileLogger, (fileLogger) => Logger.zip(Logger.prettyLoggerDefault, fileLogger) ) const LoggerLive = Logger.replaceScoped( Logger.defaultLogger, bothLoggers ).pipe(Layer.provide(NodeFileSystem.layer)) const program = Effect.log("Hello") // Run the program, writing logs to both the console (pretty format) // and "/tmp/log.txt" (logfmt) Effect.runFork(program.pipe(Effect.provide(LoggerLive))) ``` # [Runtime](https://effect.website/docs/platform/runtime/) ## Overview ## Running Your Main Program with runMain `runMain` helps you execute a main effect with built-in error handling, logging, and signal management. You can concentrate on your effect while `runMain` looks after finalizing resources, logging errors, and setting exit codes. - **Exit Codes** If your effect fails or is interrupted, `runMain` assigns a suitable exit code (for example, `1` for errors and `0` for success). - **Logs** By default, it records errors. This can be turned off if needed. - **Pretty Logging** By default, error messages are recorded using a "pretty" format. You can switch this off when required. - **Interrupt Handling** If the application receives `SIGINT` (Ctrl+C) or a similar signal, `runMain` will interrupt the effect and still run any necessary teardown steps. - **Teardown Logic** You can rely on the default teardown or define your own. The default sets an exit code of `1` for a non-interrupted failure. ### Usage Options When calling `runMain`, pass in a configuration object with these fields (all optional): - `disableErrorReporting`: If `true`, errors are not automatically logged. - `disablePrettyLogger`: If `true`, it avoids adding the "pretty" logger. - `teardown`: Provide a custom function for finalizing the program. If missing, the default sets exit code `1` for a non-interrupted failure. **Example** (Running a Successful Program) ```ts twoslash import { NodeRuntime } from "@effect/platform-node" import { Effect } from "effect" const success = Effect.succeed("Hello, World!") NodeRuntime.runMain(success) // No Output ``` **Example** (Running a Failing Program) ```ts twoslash import { NodeRuntime } from "@effect/platform-node" import { Effect } from "effect" const failure = Effect.fail("Uh oh!") NodeRuntime.runMain(failure) /* Output: [12:43:07.186] ERROR (#0): Error: Uh oh! */ ``` **Example** (Running a Failing Program Without Pretty Logger) ```ts twoslash import { NodeRuntime } from "@effect/platform-node" import { Effect } from "effect" const failure = Effect.fail("Uh oh!") NodeRuntime.runMain(failure, { disablePrettyLogger: true }) /* Output: timestamp=2025-01-14T11:43:46.276Z level=ERROR fiber=#0 cause="Error: Uh oh!" */ ``` **Example** (Running a Failing Program Without Error Reporting) ```ts twoslash import { NodeRuntime } from "@effect/platform-node" import { Effect } from "effect" const failure = Effect.fail("Uh oh!") NodeRuntime.runMain(failure, { disableErrorReporting: true }) // No Output ``` **Example** (Running a Failing Program With Custom Teardown) ```ts twoslash import { NodeRuntime } from "@effect/platform-node" import { Effect } from "effect" const failure = Effect.fail("Uh oh!") NodeRuntime.runMain(failure, { teardown: function customTeardown(exit, onExit) { if (exit._tag === "Failure") { console.error("Program ended with an error.") onExit(1) } else { console.log("Program finished successfully.") onExit(0) } } }) /* Output: [12:46:39.871] ERROR (#0): Error: Uh oh! Program ended with an error. */ ``` # [Terminal](https://effect.website/docs/platform/terminal/) ## Overview The `@effect/platform/Terminal` module provides an abstraction for interacting with standard input and output, including reading user input and displaying messages on the terminal. ## Basic Usage The module provides a single `Terminal` [tag](/docs/requirements-management/services/), which serves as the entry point to reading from and writing to standard input and standard output. **Example** (Using the Terminal Service) ```ts twoslash import { Terminal } from "@effect/platform" import { Effect } from "effect" const program = Effect.gen(function* () { const terminal = yield* Terminal.Terminal // Use `terminal` to interact with standard input and output }) ``` ## Writing to standard output **Example** (Displaying a Message on the Terminal) ```ts twoslash import { Terminal } from "@effect/platform" import { NodeRuntime, NodeTerminal } from "@effect/platform-node" import { Effect } from "effect" const program = Effect.gen(function* () { const terminal = yield* Terminal.Terminal yield* terminal.display("a message\n") }) NodeRuntime.runMain(program.pipe(Effect.provide(NodeTerminal.layer))) // Output: "a message" ``` ## Reading from standard input **Example** (Reading a Line from Standard Input) ```ts twoslash import { Terminal } from "@effect/platform" import { NodeRuntime, NodeTerminal } from "@effect/platform-node" import { Effect } from "effect" const program = Effect.gen(function* () { const terminal = yield* Terminal.Terminal const input = yield* terminal.readLine console.log(`input: ${input}`) }) NodeRuntime.runMain(program.pipe(Effect.provide(NodeTerminal.layer))) // Input: "hello" // Output: "input: hello" ``` ## Example: Number guessing game This example demonstrates how to create a complete number-guessing game by reading input from the terminal and providing feedback to the user. The game continues until the user guesses the correct number. **Example** (Interactive Number Guessing Game) ```ts twoslash import { Terminal } from "@effect/platform" import type { PlatformError } from "@effect/platform/Error" import { Effect, Option, Random } from "effect" import { NodeRuntime, NodeTerminal } from "@effect/platform-node" // Generate a secret random number between 1 and 100 const secret = Random.nextIntBetween(1, 100) // Parse the user's input into a valid number const parseGuess = (input: string) => { const n = parseInt(input, 10) return isNaN(n) || n < 1 || n > 100 ? Option.none() : Option.some(n) } // Display a message on the terminal const display = (message: string) => Effect.gen(function* () { const terminal = yield* Terminal.Terminal yield* terminal.display(`${message}\n`) }) // Prompt the user for a guess const prompt = Effect.gen(function* () { const terminal = yield* Terminal.Terminal yield* terminal.display("Enter a guess: ") return yield* terminal.readLine }) // Get the user's guess, validating it as an integer between 1 and 100 const answer: Effect.Effect< number, Terminal.QuitException | PlatformError, Terminal.Terminal > = Effect.gen(function* () { const input = yield* prompt const guess = parseGuess(input) if (Option.isNone(guess)) { yield* display("You must enter an integer from 1 to 100") return yield* answer } return guess.value }) // Check if the guess is too high, too low, or correct const check = ( secret: number, guess: number, ok: Effect.Effect, ko: Effect.Effect ) => Effect.gen(function* () { if (guess > secret) { yield* display("Too high") return yield* ko } else if (guess < secret) { yield* display("Too low") return yield* ko } else { return yield* ok } }) // End the game with a success message const end = display("You guessed it!") // Main game loop const loop = ( secret: number ): Effect.Effect< void, Terminal.QuitException | PlatformError, Terminal.Terminal > => Effect.gen(function* () { const guess = yield* answer return yield* check( secret, guess, end, Effect.suspend(() => loop(secret)) ) }) // Full game setup and execution const game = Effect.gen(function* () { yield* display( `We have selected a random number between 1 and 100. See if you can guess it in 10 turns or fewer. We'll tell you if your guess was too high or too low.` ) yield* loop(yield* secret) }) // Run the game NodeRuntime.runMain(game.pipe(Effect.provide(NodeTerminal.layer))) ``` # [Default Services](https://effect.website/docs/requirements-management/default-services/) ## Overview Effect comes equipped with five pre-built services: ```ts showLineNumbers=false type DefaultServices = Clock | ConfigProvider | Console | Random | Tracer ``` When we employ these services, there's no need to explicitly provide their implementations. Effect automatically supplies live versions of these services to our effects, sparing us from manual setup. **Example** (Using Clock and Console) ```ts twoslash import { Effect, Clock, Console } from "effect" // ┌─── Effect // ▼ const program = Effect.gen(function* () { const now = yield* Clock.currentTimeMillis yield* Console.log(`Application started at ${new Date(now)}`) }) Effect.runFork(program) // Output: Application started at ``` As you can observe, even if our program utilizes both `Clock` and `Console`, the `Requirements` parameter, representing the services required for the effect to execute, remains set to `never`. Effect takes care of handling these services seamlessly for us. ## Overriding Default Services Sometimes, you might need to replace the default services with custom implementations. Effect provides built-in utilities to override these services using `Effect.with` and `Effect.withScoped`. - `Effect.with`: Overrides a service for the duration of the effect. - `Effect.withScoped`: Overrides a service within a scope and restores the original service afterward. | Function | Description | | --------------------------------- | ------------------------------------------------------------------------------ | | `Effect.withClock` | Executes an effect using a specific `Clock` service. | | `Effect.withClockScoped` | Temporarily overrides the `Clock` service and restores it when the scope ends. | | `Effect.withConfigProvider` | Executes an effect using a specific `ConfigProvider` service. | | `Effect.withConfigProviderScoped` | Temporarily overrides the `ConfigProvider` service within a scope. | | `Effect.withConsole` | Executes an effect using a specific `Console` service. | | `Effect.withConsoleScoped` | Temporarily overrides the `Console` service within a scope. | | `Effect.withRandom` | Executes an effect using a specific `Random` service. | | `Effect.withRandomScoped` | Temporarily overrides the `Random` service within a scope. | | `Effect.withTracer` | Executes an effect using a specific `Tracer` service. | | `Effect.withTracerScoped` | Temporarily overrides the `Tracer` service within a scope. | **Example** (Overriding Random Service) ```ts twoslash import { Effect, Random } from "effect" // A program that logs a random number const program = Effect.gen(function* () { console.log(yield* Random.next) }) Effect.runSync(program) // Example Output: 0.23208633934454326 (varies each run) // Override the Random service with a seeded generator const override = program.pipe(Effect.withRandom(Random.make("myseed"))) Effect.runSync(override) // Output: 0.6862142528438508 (consistent output with the seed) ``` # [Layer Memoization](https://effect.website/docs/requirements-management/layer-memoization/) ## Overview import { Aside } from "@astrojs/starlight/components" Layer memoization allows a layer to be created once and used multiple times in the dependency graph. If we use the same layer twice: ```ts showLineNumbers=false "L1" Layer.merge(Layer.provide(L2, L1), Layer.provide(L3, L1)) ``` then the `L1` layer will be allocated only once. ## Memoization When Providing Globally One important feature of an Effect application is that layers are shared by default. This means that if the same layer is used twice, and if we provide the layer globally, the layer will only be allocated a single time. For every layer in our dependency graph, there is only one instance of it that is shared between all the layers that depend on it. **Example** For example, assume we have the three services `A`, `B`, and `C`. The implementation of both `B` and `C` is dependent on the `A` service: ```ts twoslash import { Effect, Context, Layer } from "effect" class A extends Context.Tag("A")() {} class B extends Context.Tag("B")() {} class C extends Context.Tag("C")() {} const ALive = Layer.effect( A, Effect.succeed({ a: 5 }).pipe( Effect.tap(() => Effect.log("initialized")) ) ) const BLive = Layer.effect( B, Effect.gen(function* () { const { a } = yield* A return { b: String(a) } }) ) const CLive = Layer.effect( C, Effect.gen(function* () { const { a } = yield* A return { c: a > 0 } }) ) const program = Effect.gen(function* () { yield* B yield* C }) const runnable = Effect.provide( program, Layer.merge(Layer.provide(BLive, ALive), Layer.provide(CLive, ALive)) ) Effect.runPromise(runnable) /* Output: timestamp=... level=INFO fiber=#2 message=initialized */ ``` Although both `BLive` and `CLive` layers require the `ALive` layer, the `ALive` layer is instantiated only once. It is shared with both `BLive` and `CLive`. ## Acquiring a Fresh Version If we don't want to share a module, we should create a fresh, non-shared version of it through `Layer.fresh`. **Example** ```ts twoslash import { Effect, Context, Layer } from "effect" class A extends Context.Tag("A")() {} class B extends Context.Tag("B")() {} class C extends Context.Tag("C")() {} const ALive = Layer.effect( A, Effect.succeed({ a: 5 }).pipe( Effect.tap(() => Effect.log("initialized")) ) ) const BLive = Layer.effect( B, Effect.gen(function* () { const { a } = yield* A return { b: String(a) } }) ) const CLive = Layer.effect( C, Effect.gen(function* () { const { a } = yield* A return { c: a > 0 } }) ) const program = Effect.gen(function* () { yield* B yield* C }) const runnable = Effect.provide( program, Layer.merge( Layer.provide(BLive, Layer.fresh(ALive)), Layer.provide(CLive, Layer.fresh(ALive)) ) ) Effect.runPromise(runnable) /* Output: timestamp=... level=INFO fiber=#2 message=initialized timestamp=... level=INFO fiber=#3 message=initialized */ ``` ## No Memoization When Providing Locally If we don't provide a layer globally but instead provide them locally, that layer doesn't support memoization by default. **Example** In the following example, we provided the `ALive` layer two times locally, and Effect doesn't memoize the construction of the `ALive` layer. So, it will be initialized two times: ```ts twoslash import { Effect, Context, Layer } from "effect" class A extends Context.Tag("A")() {} const Alive = Layer.effect( A, Effect.succeed({ a: 5 }).pipe( Effect.tap(() => Effect.log("initialized")) ) ) const program = Effect.gen(function* () { yield* Effect.provide(A, Alive) yield* Effect.provide(A, Alive) }) Effect.runPromise(program) /* Output: timestamp=... level=INFO fiber=#0 message=initialized timestamp=... level=INFO fiber=#0 message=initialized */ ``` ## Manual Memoization We can memoize a layer manually using the `Layer.memoize` function. It will return a scoped effect that, if evaluated, will return the lazily computed result of this layer. **Example** ```ts twoslash import { Effect, Context, Layer } from "effect" class A extends Context.Tag("A")() {} const ALive = Layer.effect( A, Effect.succeed({ a: 5 }).pipe( Effect.tap(() => Effect.log("initialized")) ) ) const program = Effect.scoped( Layer.memoize(ALive).pipe( Effect.andThen((memoized) => Effect.gen(function* () { yield* Effect.provide(A, memoized) yield* Effect.provide(A, memoized) }) ) ) ) Effect.runPromise(program) /* Output: timestamp=... level=INFO fiber=#0 message=initialized */ ``` # [Managing Layers](https://effect.website/docs/requirements-management/layers/) ## Overview import { Aside } from "@astrojs/starlight/components" In the [Managing Services](/docs/requirements-management/services/) page, you learned how to create effects which depend on some service to be provided in order to execute, as well as how to provide that service to an effect. However, what if we have a service within our effect program that has dependencies on other services in order to be built? We want to avoid leaking these implementation details into the service interface. To represent the "dependency graph" of our program and manage these dependencies more effectively, we can utilize a powerful abstraction called "Layer". Layers act as **constructors for creating services**, allowing us to manage dependencies during construction rather than at the service level. This approach helps to keep our service interfaces clean and focused. Let's review some key concepts before diving into the details: | Concept | Description | | ----------- | ------------------------------------------------------------------------------------------------------------------------- | | **service** | A reusable component providing specific functionality, used across different parts of an application. | | **tag** | A unique identifier representing a **service**, allowing Effect to locate and use it. | | **context** | A collection storing services, functioning like a map with **tags** as keys and **services** as values. | | **layer** | An abstraction for constructing **services**, managing dependencies during construction rather than at the service level. | ## Designing the Dependency Graph Let's imagine that we are building a web application. We could imagine that the dependency graph for an application where we need to manage configuration, logging, and database access might look something like this: - The `Config` service provides application configuration. - The `Logger` service depends on the `Config` service. - The `Database` service depends on both the `Config` and `Logger` services. Our goal is to build the `Database` service along with its direct and indirect dependencies. This means we need to ensure that the `Config` service is available for both `Logger` and `Database`, and then provide these dependencies to the `Database` service. ## Avoiding Requirement Leakage When constructing the `Database` service, it's important to avoid exposing the dependencies on `Config` and `Logger` within the `Database` interface. You might be tempted to define the `Database` service as follows: **Example** (Leaking Dependencies in the Service Interface) ```ts twoslash "Config | Logger" import { Effect, Context } from "effect" // Declaring a tag for the Config service class Config extends Context.Tag("Config")() {} // Declaring a tag for the Logger service class Logger extends Context.Tag("Logger")() {} // Declaring a tag for the Database service class Database extends Context.Tag("Database")< Database, { // ❌ Avoid exposing Config and Logger as a requirement readonly query: ( sql: string ) => Effect.Effect } >() {} ``` Here, the `query` function of the `Database` service requires both `Config` and `Logger`. This design leaks implementation details, making the `Database` service aware of its dependencies, which complicates testing and makes it difficult to mock. To demonstrate the problem, let's create a test instance of the `Database` service: **Example** (Creating a Test Instance with Leaked Dependencies) ```ts twoslash collapse={3-17} import { Effect, Context } from "effect" // Declaring a tag for the Config service class Config extends Context.Tag("Config")() {} // Declaring a tag for the Logger service class Logger extends Context.Tag("Logger")() {} // Declaring a tag for the Database service class Database extends Context.Tag("Database")< Database, { readonly query: ( sql: string ) => Effect.Effect } >() {} // Declaring a test instance of the Database service const DatabaseTest = Database.of({ // Simulating a simple response query: (sql: string) => Effect.succeed([]) }) import * as assert from "node:assert" // A test that uses the Database service const test = Effect.gen(function* () { const database = yield* Database const result = yield* database.query("SELECT * FROM users") assert.deepStrictEqual(result, []) }) // ┌─── Effect // ▼ const incompleteTestSetup = test.pipe( // Attempt to provide only the Database service without Config and Logger Effect.provideService(Database, DatabaseTest) ) ``` Because the `Database` service interface directly includes dependencies on `Config` and `Logger`, it forces any test setup to include these services, even if they're irrelevant to the test. This adds unnecessary complexity and makes it difficult to write simple, isolated unit tests. Instead of directly tying dependencies to the `Database` service interface, dependencies should be managed at the construction phase. We can use **layers** to properly construct the `Database` service and manage its dependencies without leaking details into the interface. ## Creating Layers The `Layer` type is structured as follows: ```text showLineNumbers=false ┌─── The service to be created │ ┌─── The possible error │ │ ┌─── The required dependencies ▼ ▼ ▼ Layer ``` A `Layer` represents a blueprint for constructing a `RequirementsOut` (the service). It requires a `RequirementsIn` (dependencies) as input and may result in an error of type `Error` during the construction process. | Parameter | Description | | ----------------- | -------------------------------------------------------------------------- | | `RequirementsOut` | The service or resource to be created. | | `Error` | The type of error that might occur during the construction of the service. | | `RequirementsIn` | The dependencies required to construct the service. | By using layers, you can better organize your services, ensuring that their dependencies are clearly defined and separated from their implementation details. For simplicity, let's assume that we won't encounter any errors during the value construction (meaning `Error = never`). Now, let's determine how many layers we need to implement our dependency graph: | Layer | Dependencies | Type | | -------------- | ---------------------------------------------------------- | ------------------------------------------ | | `ConfigLive` | The `Config` service does not depend on any other services | `Layer` | | `LoggerLive` | The `Logger` service depends on the `Config` service | `Layer` | | `DatabaseLive` | The `Database` service depends on `Config` and `Logger` | `Layer` | When a service has multiple dependencies, they are represented as a **union type**. In our case, the `Database` service depends on both the `Config` and `Logger` services. Therefore, the type for the `DatabaseLive` layer will be: ```ts showLineNumbers=false "Config | Logger" Layer ``` ### Config The `Config` service does not depend on any other services, so `ConfigLive` will be the simplest layer to implement. Just like in the [Managing Services](/docs/requirements-management/services/) page, we must create a tag for the service. And because the service has no dependencies, we can create the layer directly using the `Layer.succeed` constructor: ```ts twoslash import { Effect, Context, Layer } from "effect" // Declaring a tag for the Config service class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect<{ readonly logLevel: string readonly connection: string }> } >() {} // Layer const ConfigLive = Layer.succeed( Config, Config.of({ getConfig: Effect.succeed({ logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name" }) }) ) ``` Looking at the type of `ConfigLive` we can observe: - `RequirementsOut` is `Config`, indicating that constructing the layer will produce a `Config` service - `Error` is `never`, indicating that layer construction cannot fail - `RequirementsIn` is `never`, indicating that the layer has no dependencies Note that, to construct `ConfigLive`, we used the `Config.of` constructor. However, this is merely a helper to ensure correct type inference for the implementation. It's possible to skip this helper and construct the implementation directly as a simple object: ```ts twoslash collapse={4-12} import { Effect, Context, Layer } from "effect" // Declaring a tag for the Config service class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect<{ readonly logLevel: string readonly connection: string }> } >() {} // Layer const ConfigLive = Layer.succeed(Config, { getConfig: Effect.succeed({ logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name" }) }) ``` ### Logger Now we can move on to the implementation of the `Logger` service, which depends on the `Config` service to retrieve some configuration. Just like we did in the [Managing Services](/docs/requirements-management/services/#using-the-service) page, we can yield the `Config` tag to "extract" the service from the context. Given that using the `Config` tag is an effectful operation, we use `Layer.effect` to create a layer from the resulting effect. ```ts twoslash collapse={4-20} import { Effect, Context, Layer } from "effect" // Declaring a tag for the Config service class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect<{ readonly logLevel: string readonly connection: string }> } >() {} // Layer const ConfigLive = Layer.succeed(Config, { getConfig: Effect.succeed({ logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name" }) }) // Declaring a tag for the Logger service class Logger extends Context.Tag("Logger")< Logger, { readonly log: (message: string) => Effect.Effect } >() {} // Layer const LoggerLive = Layer.effect( Logger, Effect.gen(function* () { const config = yield* Config return { log: (message) => Effect.gen(function* () { const { logLevel } = yield* config.getConfig console.log(`[${logLevel}] ${message}`) }) } }) ) ``` Looking at the type of `LoggerLive`: ```ts showLineNumbers=false Layer ``` we can observe that: - `RequirementsOut` is `Logger` - `Error` is `never`, indicating that layer construction cannot fail - `RequirementsIn` is `Config`, indicating that the layer has a requirement ### Database Finally, we can use our `Config` and `Logger` services to implement the `Database` service. ```ts twoslash collapse={4-20,23-41} import { Effect, Context, Layer } from "effect" // Declaring a tag for the Config service class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect<{ readonly logLevel: string readonly connection: string }> } >() {} // Layer const ConfigLive = Layer.succeed(Config, { getConfig: Effect.succeed({ logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name" }) }) // Declaring a tag for the Logger service class Logger extends Context.Tag("Logger")< Logger, { readonly log: (message: string) => Effect.Effect } >() {} // Layer const LoggerLive = Layer.effect( Logger, Effect.gen(function* () { const config = yield* Config return { log: (message) => Effect.gen(function* () { const { logLevel } = yield* config.getConfig console.log(`[${logLevel}] ${message}`) }) } }) ) // Declaring a tag for the Database service class Database extends Context.Tag("Database")< Database, { readonly query: (sql: string) => Effect.Effect } >() {} // Layer const DatabaseLive = Layer.effect( Database, Effect.gen(function* () { const config = yield* Config const logger = yield* Logger return { query: (sql: string) => Effect.gen(function* () { yield* logger.log(`Executing query: ${sql}`) const { connection } = yield* config.getConfig return { result: `Results from ${connection}` } }) } }) ) ``` Looking at the type of `DatabaseLive`: ```ts showLineNumbers=false Layer ``` we can observe that the `RequirementsIn` type is `Config | Logger`, i.e., the `Database` service requires both `Config` and `Logger` services. ## Combining Layers Layers can be combined in two primary ways: **merging** and **composing**. ### Merging Layers Layers can be combined through merging using the `Layer.merge` function: ```ts twoslash import { Layer } from "effect" declare const layer1: Layer.Layer<"Out1", never, "In1"> declare const layer2: Layer.Layer<"Out2", never, "In2"> // Layer<"Out1" | "Out2", never, "In1" | "In2"> const merging = Layer.merge(layer1, layer2) ``` When we merge two layers, the resulting layer: - requires all the services that both of them require (`"In1" | "In2"`). - produces all services that both of them produce (`"Out1" | "Out2"`). For example, in our web application above, we can merge our `ConfigLive` and `LoggerLive` layers into a single `AppConfigLive` layer, which retains the requirements of both layers (`never | Config = Config`) and the outputs of both layers (`Config | Logger`): ```ts twoslash collapse={4-20,23-41} import { Effect, Context, Layer } from "effect" // Declaring a tag for the Config service class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect<{ readonly logLevel: string readonly connection: string }> } >() {} // Layer const ConfigLive = Layer.succeed(Config, { getConfig: Effect.succeed({ logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name" }) }) // Declaring a tag for the Logger service class Logger extends Context.Tag("Logger")< Logger, { readonly log: (message: string) => Effect.Effect } >() {} // Layer const LoggerLive = Layer.effect( Logger, Effect.gen(function* () { const config = yield* Config return { log: (message) => Effect.gen(function* () { const { logLevel } = yield* config.getConfig console.log(`[${logLevel}] ${message}`) }) } }) ) // Layer const AppConfigLive = Layer.merge(ConfigLive, LoggerLive) ``` ### Composing Layers Layers can be composed using the `Layer.provide` function: ```ts twoslash import { Layer } from "effect" declare const inner: Layer.Layer<"OutInner", never, "InInner"> declare const outer: Layer.Layer<"InInner", never, "InOuter"> // Layer<"OutInner", never, "InOuter"> const composition = Layer.provide(inner, outer) ``` Sequential composition of layers implies that the output of one layer is supplied as the input for the inner layer, resulting in a single layer with the requirements of the outer layer and the output of the inner. Now we can compose the `AppConfigLive` layer with the `DatabaseLive` layer: ```ts twoslash collapse={4-20,23-41,44-64} import { Effect, Context, Layer } from "effect" // Declaring a tag for the Config service class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect<{ readonly logLevel: string readonly connection: string }> } >() {} // Layer const ConfigLive = Layer.succeed(Config, { getConfig: Effect.succeed({ logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name" }) }) // Declaring a tag for the Logger service class Logger extends Context.Tag("Logger")< Logger, { readonly log: (message: string) => Effect.Effect } >() {} // Layer const LoggerLive = Layer.effect( Logger, Effect.gen(function* () { const config = yield* Config return { log: (message) => Effect.gen(function* () { const { logLevel } = yield* config.getConfig console.log(`[${logLevel}] ${message}`) }) } }) ) // Declaring a tag for the Database service class Database extends Context.Tag("Database")< Database, { readonly query: (sql: string) => Effect.Effect } >() {} // Layer const DatabaseLive = Layer.effect( Database, Effect.gen(function* () { const config = yield* Config const logger = yield* Logger return { query: (sql: string) => Effect.gen(function* () { yield* logger.log(`Executing query: ${sql}`) const { connection } = yield* config.getConfig return { result: `Results from ${connection}` } }) } }) ) // Layer const AppConfigLive = Layer.merge(ConfigLive, LoggerLive) // Layer const MainLive = DatabaseLive.pipe( // provides the config and logger to the database Layer.provide(AppConfigLive), // provides the config to AppConfigLive Layer.provide(ConfigLive) ) ``` We obtained a `MainLive` layer that produces the `Database` service: ```ts showLineNumbers=false Layer ``` This layer is the fully resolved layer for our application. ### Merging and Composing Layers Let's say we want our `MainLive` layer to return both the `Config` and `Database` services. We can achieve this with `Layer.provideMerge`: ```ts twoslash collapse={4-19,22-39,42-61} import { Effect, Context, Layer } from "effect" // Declaring a tag for the Config service class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect<{ readonly logLevel: string readonly connection: string }> } >() {} const ConfigLive = Layer.succeed(Config, { getConfig: Effect.succeed({ logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name" }) }) // Declaring a tag for the Logger service class Logger extends Context.Tag("Logger")< Logger, { readonly log: (message: string) => Effect.Effect } >() {} const LoggerLive = Layer.effect( Logger, Effect.gen(function* () { const config = yield* Config return { log: (message) => Effect.gen(function* () { const { logLevel } = yield* config.getConfig console.log(`[${logLevel}] ${message}`) }) } }) ) // Declaring a tag for the Database service class Database extends Context.Tag("Database")< Database, { readonly query: (sql: string) => Effect.Effect } >() {} const DatabaseLive = Layer.effect( Database, Effect.gen(function* () { const config = yield* Config const logger = yield* Logger return { query: (sql: string) => Effect.gen(function* () { yield* logger.log(`Executing query: ${sql}`) const { connection } = yield* config.getConfig return { result: `Results from ${connection}` } }) } }) ) // Layer const AppConfigLive = Layer.merge(ConfigLive, LoggerLive) // Layer const MainLive = DatabaseLive.pipe( Layer.provide(AppConfigLive), Layer.provideMerge(ConfigLive) ) ``` ## Providing a Layer to an Effect Now that we have assembled the fully resolved `MainLive` for our application, we can provide it to our program to satisfy the program's requirements using `Effect.provide`: ```ts twoslash collapse={3-65} import { Effect, Context, Layer } from "effect" class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect<{ readonly logLevel: string readonly connection: string }> } >() {} const ConfigLive = Layer.succeed(Config, { getConfig: Effect.succeed({ logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name" }) }) class Logger extends Context.Tag("Logger")< Logger, { readonly log: (message: string) => Effect.Effect } >() {} const LoggerLive = Layer.effect( Logger, Effect.gen(function* () { const config = yield* Config return { log: (message) => Effect.gen(function* () { const { logLevel } = yield* config.getConfig console.log(`[${logLevel}] ${message}`) }) } }) ) class Database extends Context.Tag("Database")< Database, { readonly query: (sql: string) => Effect.Effect } >() {} const DatabaseLive = Layer.effect( Database, Effect.gen(function* () { const config = yield* Config const logger = yield* Logger return { query: (sql: string) => Effect.gen(function* () { yield* logger.log(`Executing query: ${sql}`) const { connection } = yield* config.getConfig return { result: `Results from ${connection}` } }) } }) ) const AppConfigLive = Layer.merge(ConfigLive, LoggerLive) const MainLive = DatabaseLive.pipe( Layer.provide(AppConfigLive), Layer.provide(ConfigLive) ) // ┌─── Effect // ▼ const program = Effect.gen(function* () { const database = yield* Database const result = yield* database.query("SELECT * FROM users") return result }) // ┌─── Effect // ▼ const runnable = Effect.provide(program, MainLive) Effect.runPromise(runnable).then(console.log) /* Output: [INFO] Executing query: SELECT * FROM users { result: 'Results from mysql://username:password@hostname:port/database_name' } */ ``` Note that the `runnable` requirements type is `never`, indicating that the program does not require any additional services to run. ## Converting a Layer to an Effect Sometimes your entire application might be a Layer, for example, an HTTP server. You can convert that layer to an effect with `Layer.launch`. It constructs the layer and keeps it alive until interrupted. **Example** (Launching an HTTP Server Layer) ```ts twoslash import { Console, Context, Effect, Layer } from "effect" class HTTPServer extends Context.Tag("HTTPServer")() {} // Simulating an HTTP server const server = Layer.effect( HTTPServer, // Log a message to simulate a server starting Console.log("Listening on http://localhost:3000") ) // Converts the layer to an effect and runs it Effect.runFork(Layer.launch(server)) /* Output: Listening on http://localhost:3000 ... */ ``` ## Tapping The `Layer.tap` and `Layer.tapError` functions allow you to perform additional effects based on the success or failure of a layer. These operations do not modify the layer's signature but are useful for logging or performing side effects during layer construction. - `Layer.tap`: Executes a specified effect when the layer is successfully acquired. - `Layer.tapError`: Executes a specified effect when the layer fails to acquire. **Example** (Logging Success and Failure During Layer Acquisition) ```ts twoslash import { Config, Context, Effect, Layer, Console } from "effect" class HTTPServer extends Context.Tag("HTTPServer")() {} // Simulating an HTTP server const server = Layer.effect( HTTPServer, Effect.gen(function* () { const host = yield* Config.string("HOST") console.log(`Listening on http://localhost:${host}`) }) ).pipe( // Log a message if the layer acquisition succeeds Layer.tap((ctx) => Console.log(`layer acquisition succeeded with:\n${ctx}`) ), // Log a message if the layer acquisition fails Layer.tapError((err) => Console.log(`layer acquisition failed with:\n${err}`) ) ) Effect.runFork(Layer.launch(server)) /* Output: layer acquisition failed with: (Missing data at HOST: "Expected HOST to exist in the process context") */ ``` ## Error Handling When constructing layers, it is important to handle potential errors. The Effect library provides tools like `Layer.catchAll` and `Layer.orElse` to manage errors and define fallback layers in case of failure. ### catchAll The `Layer.catchAll` function allows you to recover from errors during layer construction by specifying a fallback layer. This can be useful for handling specific error cases and ensuring the application can continue with an alternative setup. **Example** (Recovering from Errors During Layer Construction) ```ts twoslash import { Config, Context, Effect, Layer } from "effect" class HTTPServer extends Context.Tag("HTTPServer")() {} // Simulating an HTTP server const server = Layer.effect( HTTPServer, Effect.gen(function* () { const host = yield* Config.string("HOST") console.log(`Listening on http://localhost:${host}`) }) ).pipe( // Recover from errors during layer construction Layer.catchAll((configError) => Layer.effect( HTTPServer, Effect.gen(function* () { console.log(`Recovering from error:\n${configError}`) console.log(`Listening on http://localhost:3000`) }) ) ) ) Effect.runFork(Layer.launch(server)) /* Output: Recovering from error: (Missing data at HOST: "Expected HOST to exist in the process context") Listening on http://localhost:3000 ... */ ``` ### orElse The `Layer.orElse` function provides a simpler way to fall back to an alternative layer if the initial layer fails. Unlike `Layer.catchAll`, it does not receive the error as input. Use this when you only need to provide a default layer without reacting to specific errors. **Example** (Fallback to an Alternative Layer) ```ts twoslash import { Config, Context, Effect, Layer } from "effect" class Database extends Context.Tag("Database")() {} // Simulating a database connection const postgresDatabaseLayer = Layer.effect( Database, Effect.gen(function* () { const databaseConnectionString = yield* Config.string( "CONNECTION_STRING" ) console.log( `Connecting to database with: ${databaseConnectionString}` ) }) ) // Simulating an in-memory database connection const inMemoryDatabaseLayer = Layer.effect( Database, Effect.gen(function* () { console.log(`Connecting to in-memory database`) }) ) // Fallback to in-memory database if PostgreSQL connection fails const database = postgresDatabaseLayer.pipe( Layer.orElse(() => inMemoryDatabaseLayer) ) Effect.runFork(Layer.launch(database)) /* Output: Connecting to in-memory database ... */ ``` ## Simplifying Service Definitions with Effect.Service The `Effect.Service` API provides a way to define a service in a single step, including its tag and layer. It also allows specifying dependencies upfront, making service construction more straightforward. ### Defining a Service with Dependencies The following example defines a `Cache` service that depends on a file system. **Example** (Defining a Cache Service) ```ts twoslash import { FileSystem } from "@effect/platform" import { NodeFileSystem } from "@effect/platform-node" import { Effect } from "effect" // Define a Cache service class Cache extends Effect.Service()("app/Cache", { // Define how to create the service effect: Effect.gen(function* () { const fs = yield* FileSystem.FileSystem const lookup = (key: string) => fs.readFileString(`cache/${key}`) return { lookup } as const }), // Specify dependencies dependencies: [NodeFileSystem.layer] }) {} ``` ### Using the Generated Layers The `Effect.Service` API automatically generates layers for the service. | Layer | Description | | ---------------------------------- | --------------------------------------------------------------------------------- | | `Cache.Default` | Provides the `Cache` service with its dependencies already included. | | `Cache.DefaultWithoutDependencies` | Provides the `Cache` service but requires dependencies to be provided separately. | ```ts twoslash collapse={6-13} import { FileSystem } from "@effect/platform" import { NodeFileSystem } from "@effect/platform-node" import { Effect } from "effect" // Define a Cache service class Cache extends Effect.Service()("app/Cache", { effect: Effect.gen(function* () { const fs = yield* FileSystem.FileSystem const lookup = (key: string) => fs.readFileString(`cache/${key}`) return { lookup } as const }), dependencies: [NodeFileSystem.layer] }) {} // Layer that includes all required dependencies // // ┌─── Layer // ▼ const layer = Cache.Default // Layer without dependencies, requiring them to be provided externally // // ┌─── Layer.Layer // ▼ const layerNoDeps = Cache.DefaultWithoutDependencies ``` ### Accessing the Service A service created with `Effect.Service` can be accessed like any other Effect service. **Example** (Accessing the Cache Service) ```ts twoslash collapse={6-13} import { FileSystem } from "@effect/platform" import { NodeFileSystem } from "@effect/platform-node" import { Effect, Console } from "effect" // Define a Cache service class Cache extends Effect.Service()("app/Cache", { effect: Effect.gen(function* () { const fs = yield* FileSystem.FileSystem const lookup = (key: string) => fs.readFileString(`cache/${key}`) return { lookup } as const }), dependencies: [NodeFileSystem.layer] }) {} // Accessing the Cache Service const program = Effect.gen(function* () { const cache = yield* Cache const data = yield* cache.lookup("my-key") console.log(data) }).pipe(Effect.catchAllCause((cause) => Console.log(cause))) const runnable = program.pipe(Effect.provide(Cache.Default)) Effect.runFork(runnable) /* { _id: 'Cause', _tag: 'Fail', failure: { _tag: 'SystemError', reason: 'NotFound', module: 'FileSystem', method: 'readFile', pathOrDescriptor: 'cache/my-key', syscall: 'open', message: "ENOENT: no such file or directory, open 'cache/my-key'", [Symbol(@effect/platform/Error/PlatformErrorTypeId)]: Symbol(@effect/platform/Error/PlatformErrorTypeId) } } */ ``` Since this example uses `Cache.Default`, it interacts with the real file system. If the file does not exist, it results in an error. ### Injecting Test Dependencies To test the program without depending on the real file system, we can inject a test file system using the `Cache.DefaultWithoutDependencies` layer. **Example** (Using a Test File System) ```ts twoslash collapse={6-13,16-20} import { FileSystem } from "@effect/platform" import { NodeFileSystem } from "@effect/platform-node" import { Effect, Console } from "effect" // Define a Cache service class Cache extends Effect.Service()("app/Cache", { effect: Effect.gen(function* () { const fs = yield* FileSystem.FileSystem const lookup = (key: string) => fs.readFileString(`cache/${key}`) return { lookup } as const }), dependencies: [NodeFileSystem.layer] }) {} // Accessing the Cache Service const program = Effect.gen(function* () { const cache = yield* Cache const data = yield* cache.lookup("my-key") console.log(data) }).pipe(Effect.catchAllCause((cause) => Console.log(cause))) // Create a test file system that always returns a fixed value const FileSystemTest = FileSystem.layerNoop({ readFileString: () => Effect.succeed("File Content...") }) const runnable = program.pipe( Effect.provide(Cache.DefaultWithoutDependencies), // Provide the mock file system Effect.provide(FileSystemTest) ) Effect.runFork(runnable) // Output: File Content... ``` ### Mocking the Service Directly Alternatively, you can mock the `Cache` service itself instead of replacing its dependencies. **Example** (Mocking the Cache Service) ```ts twoslash collapse={6-13,16-20} import { FileSystem } from "@effect/platform" import { NodeFileSystem } from "@effect/platform-node" import { Effect, Console } from "effect" // Define a Cache service class Cache extends Effect.Service()("app/Cache", { effect: Effect.gen(function* () { const fs = yield* FileSystem.FileSystem const lookup = (key: string) => fs.readFileString(`cache/${key}`) return { lookup } as const }), dependencies: [NodeFileSystem.layer] }) {} // Accessing the Cache Service const program = Effect.gen(function* () { const cache = yield* Cache const data = yield* cache.lookup("my-key") console.log(data) }).pipe(Effect.catchAllCause((cause) => Console.log(cause))) // Create a mock implementation of Cache const cache = new Cache({ lookup: () => Effect.succeed("Cache Content...") }) // Provide the mock Cache service const runnable = program.pipe(Effect.provideService(Cache, cache)) Effect.runFork(runnable) // Output: File Content... ``` ### Alternative Ways to Define a Service The `Effect.Service` API supports multiple ways to define a service: | Method | Description | | --------- | -------------------------------------------------- | | `effect` | Defines a service with an effectful constructor. | | `sync` | Defines a service using a synchronous constructor. | | `succeed` | Provides a static implementation of the service. | | `scoped` | Creates a service with lifecycle management. | **Example** (Defining a Service with a Static Implementation) ```ts twoslash import { Effect, Random } from "effect" class Sync extends Effect.Service()("Sync", { sync: () => ({ next: Random.nextInt }) }) {} // ┌─── Effect // ▼ const program = Effect.gen(function* () { const sync = yield* Sync const n = yield* sync.next console.log(`The number is ${n}`) }) Effect.runPromise(program.pipe(Effect.provide(Sync.Default))) // Example Output: The number is 3858843290019673 ``` **Example** (Managing a Service with Lifecycle Control) ```ts twoslash import { Effect, Console } from "effect" class Scoped extends Effect.Service()("Scoped", { scoped: Effect.gen(function* () { // Acquire the resource and ensure it is properly released const resource = yield* Effect.acquireRelease( Console.log("Aquiring...").pipe(Effect.as("foo")), () => Console.log("Releasing...") ) // Register a finalizer to run when the effect is completed yield* Effect.addFinalizer(() => Console.log("Shutting down")) return { resource } }) }) {} // ┌─── Effect // ▼ const program = Effect.gen(function* () { const resource = (yield* Scoped).resource console.log(`The resource is ${resource}`) }) Effect.runPromise( program.pipe( Effect.provide( // ┌─── Layer // ▼ Scoped.Default ) ) ) /* Aquiring... The resource is foo Shutting down Releasing... */ ``` The `Scoped.Default` layer does not require `Scope` as a dependency, since `Scoped` itself manages its lifecycle. ### Enabling Direct Method Access By setting `accessors: true`, you can call service methods directly using the service tag instead of first extracting the service. **Example** (Defining a Service with Direct Method Access) ```ts twoslash del={11-12} ins={13} import { Effect, Random } from "effect" class Sync extends Effect.Service()("Sync", { sync: () => ({ next: Random.nextInt }), accessors: true // Enables direct method access via the tag }) {} const program = Effect.gen(function* () { // const sync = yield* Sync // const n = yield* sync.next const n = yield* Sync.next // No need to extract the service first console.log(`The number is ${n}`) }) Effect.runPromise(program.pipe(Effect.provide(Sync.Default))) // Example Output: The number is 3858843290019673 ``` # [Managing Services](https://effect.website/docs/requirements-management/services/) ## Overview import { Aside, Tabs, TabItem } from "@astrojs/starlight/components" In the context of programming, a **service** refers to a reusable component or functionality that can be used by different parts of an application. Services are designed to provide specific capabilities and can be shared across multiple modules or components. Services often encapsulate common tasks or operations that are needed by different parts of an application. They can handle complex operations, interact with external systems or APIs, manage data, or perform other specialized tasks. Services are typically designed to be modular and decoupled from the rest of the application. This allows them to be easily maintained, tested, and replaced without affecting the overall functionality of the application. When diving into services and their integration in application development, it helps to start from the basic principles of function management and dependency handling without relying on advanced constructs. Imagine having to manually pass a service around to every function that needs it: ```ts showLineNumbers=false const processData = (data: Data, databaseService: DatabaseService) => { // Operations using the database service } ``` This approach becomes cumbersome and unmanageable as your application grows, with services needing to be passed through multiple layers of functions. To streamline this, you might consider using an environment object that bundles various services: ```ts showLineNumbers=false type Context = { databaseService: DatabaseService loggingService: LoggingService } const processData = (data: Data, context: Context) => { // Using multiple services from the context } ``` However, this introduces a new complexity: you must ensure that the environment is correctly set up with all necessary services before it's used, which can lead to tightly coupled code and makes functional composition and testing more difficult. ## Managing Services with Effect The Effect library simplifies managing these dependencies by leveraging the type system. Instead of manually passing services or environment objects around, Effect allows you to declare service dependencies directly in the function's type signature using the `Requirements` parameter in the `Effect` type: ```ts showLineNumbers=false "Requirements" ┌─── Represents required dependencies ▼ Effect ``` This is how it works in practice when using Effect: **Dependency Declaration**: You specify what services a function needs directly in its type, pushing the complexity of dependency management into the type system. **Service Provision**: `Effect.provideService` is used to make a service implementation available to the functions that need it. By providing services at the start, you ensure that all parts of your application have consistent access to the required services, thus maintaining a clean and decoupled architecture. This approach abstracts away manual service handling, letting developers focus on business logic while the compiler ensures all dependencies are correctly managed. It also makes code more maintainable and scalable. Let's walk through managing services in Effect step by step: 1. **Creating a Service**: Define a service with its unique functionality and interface. 2. **Using the Service**: Access and utilize the service within your application’s functions. 3. **Providing a Service Implementation**: Supply an actual implementation of the service to fulfill the declared requirements. ## How It Works Up to this point, our examples with the Effect framework have dealt with effects that operate independently of external services. This means the `Requirements` parameter in our `Effect` type signature has been set to `never`, indicating no dependencies. However, real-world applications often need effects that rely on specific services to function correctly. These services are managed and accessed through a construct known as `Context`. The `Context` serves as a repository or container for all services an effect may require. It acts like a store that maintains these services, allowing various parts of your application to access and use them as needed. The services stored within the `Context` are directly reflected in the `Requirements` parameter of the `Effect` type. Each service within the `Context` is identified by a unique "tag," which is essentially a unique identifier for the service. When an effect needs to use a specific service, the service's tag is included in the `Requirements` type parameter. ## Creating a Service To create a new service, you need two things: 1. A unique **identifier**. 2. A **type** describing the possible operations of the service. **Example** (Defining a Random Number Generator Service) Let's create a service for generating random numbers. 1. **Identifier**. We'll use the string `"MyRandomService"` as the unique identifier. 2. **Type**. The service type will have a single operation called `next` that returns a random number. ```ts twoslash import { Effect, Context } from "effect" // Declaring a tag for a service that generates random numbers class Random extends Context.Tag("MyRandomService")< Random, { readonly next: Effect.Effect } >() {} ``` The exported `Random` value is known as a **tag** in Effect. It acts as a representation of the service and allows Effect to locate and use this service at runtime. The service will be stored in a collection called `Context`, which can be thought of as a `Map` where the keys are tags and the values are services: ```ts showLineNumbers=false type Context = Map ``` Let's summarize the concepts we've covered so far: | Concept | Description | | ----------- | ------------------------------------------------------------------------------------------------------ | | **service** | A reusable component providing specific functionality, used across different parts of an application. | | **tag** | A unique identifier representing a **service**, allowing Effect to locate and use it. | | **context** | A collection storing service, functioning like a map with **tags** as keys and **services** as values. | ## Using the Service Now that we have our service tag defined, let's see how we can use it by building a simple program. **Example** (Using the Random Service) ```ts twoslash import { Effect, Context } from "effect" // Declaring a tag for a service that generates random numbers class Random extends Context.Tag("MyRandomService")< Random, { readonly next: Effect.Effect } >() {} // Using the service // // ┌─── Effect // ▼ const program = Effect.gen(function* () { const random = yield* Random const randomNumber = yield* random.next console.log(`random number: ${randomNumber}`) }) ``` In the code above, we can observe that we are able to yield the `Random` tag as if it were an effect itself. This allows us to access the `next` operation of the service. ```ts twoslash import { Effect, Context, Console } from "effect" // Declaring a tag for a service that generates random numbers class Random extends Context.Tag("MyRandomService")< Random, { readonly next: Effect.Effect } >() {} // Using the service // // ┌─── Effect // ▼ const program = Random.pipe( Effect.andThen((random) => random.next), Effect.andThen((randomNumber) => Console.log(`random number: ${randomNumber}`) ) ) ``` In the code above, we can observe that we are able to flat-map over the `Random` tag as if it were an effect itself. This allows us to access the `next` operation of the service within the `Effect.andThen` callback. It's worth noting that the type of the `program` variable includes `Random` in the `Requirements` type parameter: ```ts "Random" showLineNumbers=false const program: Effect ``` This indicates that our program requires the `Random` service to be provided in order to execute successfully. If we attempt to execute the effect without providing the necessary service we will encounter a type-checking error: **Example** (Type Error Without Service Provision) ```ts twoslash import { Effect, Context } from "effect" // Declaring a tag for a service that generates random numbers class Random extends Context.Tag("MyRandomService")< Random, { readonly next: Effect.Effect } >() {} // Using the service const program = Effect.gen(function* () { const random = yield* Random const randomNumber = yield* random.next console.log(`random number: ${randomNumber}`) }) // @ts-expect-error Effect.runSync(program) /* Argument of type 'Effect' is not assignable to parameter of type 'Effect'. Type 'Random' is not assignable to type 'never'.ts(2345) */ ``` To resolve this error and successfully execute the program, we need to provide an actual implementation of the `Random` service. In the next section, we will explore how to implement and provide the `Random` service to our program, enabling us to run it successfully. ## Providing a Service Implementation In order to provide an actual implementation of the `Random` service, we can utilize the `Effect.provideService` function. **Example** (Providing a Random Number Implementation) ```ts twoslash import { Effect, Context } from "effect" // Declaring a tag for a service that generates random numbers class Random extends Context.Tag("MyRandomService")< Random, { readonly next: Effect.Effect } >() {} // Using the service const program = Effect.gen(function* () { const random = yield* Random const randomNumber = yield* random.next console.log(`random number: ${randomNumber}`) }) // Providing the implementation // // ┌─── Effect // ▼ const runnable = Effect.provideService(program, Random, { next: Effect.sync(() => Math.random()) }) // Run successfully Effect.runPromise(runnable) /* Example Output: random number: 0.8241872233134417 */ ``` In the code above, we provide the `program` we defined earlier with an implementation of the `Random` service. We use the `Effect.provideService` function to associate the `Random` tag with its implementation, an object with a `next` operation that generates a random number. Notice that the `Requirements` type parameter of the `runnable` effect is now `never`. This indicates that the effect no longer requires any service to be provided. With the implementation of the `Random` service in place, we are able to run the program without any further requirements. ## Extracting the Service Type To retrieve the service type from a tag, use the `Context.Tag.Service` utility type. **Example** (Extracting Service Type) ```ts twoslash import { Effect, Context } from "effect" // Declaring a tag class Random extends Context.Tag("MyRandomService")< Random, { readonly next: Effect.Effect } >() {} // Extracting the type type RandomShape = Context.Tag.Service /* This is equivalent to: type RandomShape = { readonly next: Effect.Effect; } */ ``` ## Using Multiple Services When we require the usage of more than one service, the process remains similar to what we've learned in defining a service, repeated for each service needed. **Example** (Using Random and Logger Services) Let's examine an example where we need two services, namely `Random` and `Logger`: ```ts twoslash import { Effect, Context } from "effect" // Declaring a tag for a service that generates random numbers class Random extends Context.Tag("MyRandomService")< Random, { readonly next: Effect.Effect } >() {} // Declaring a tag for the logging service class Logger extends Context.Tag("MyLoggerService")< Logger, { readonly log: (message: string) => Effect.Effect } >() {} const program = Effect.gen(function* () { // Acquire instances of the 'Random' and 'Logger' services const random = yield* Random const logger = yield* Logger const randomNumber = yield* random.next yield* logger.log(String(randomNumber)) }) ``` The `program` effect now has a `Requirements` type parameter of `Random | Logger`: ```ts showLineNumbers=false "Random | Logger" const program: Effect ``` indicating that it requires both the `Random` and `Logger` services to be provided. To execute the `program`, we need to provide implementations for both services: **Example** (Providing Multiple Services) ```ts twoslash collapse={3-24} import { Effect, Context } from "effect" // Declaring a tag for a service that generates random numbers class Random extends Context.Tag("MyRandomService")< Random, { readonly next: Effect.Effect } >() {} // Declaring a tag for the logging service class Logger extends Context.Tag("MyLoggerService")< Logger, { readonly log: (message: string) => Effect.Effect } >() {} const program = Effect.gen(function* () { const random = yield* Random const logger = yield* Logger const randomNumber = yield* random.next return yield* logger.log(String(randomNumber)) }) // Provide service implementations for 'Random' and 'Logger' const runnable = program.pipe( Effect.provideService(Random, { next: Effect.sync(() => Math.random()) }), Effect.provideService(Logger, { log: (message) => Effect.sync(() => console.log(message)) }) ) ``` Alternatively, instead of calling `provideService` multiple times, we can combine the service implementations into a single `Context` and then provide the entire context using the `Effect.provide` function: **Example** (Combining Service Implementations) ```ts twoslash collapse={3-24} import { Effect, Context } from "effect" // Declaring a tag for a service that generates random numbers class Random extends Context.Tag("MyRandomService")< Random, { readonly next: Effect.Effect } >() {} // Declaring a tag for the logging service class Logger extends Context.Tag("MyLoggerService")< Logger, { readonly log: (message: string) => Effect.Effect } >() {} const program = Effect.gen(function* () { const random = yield* Random const logger = yield* Logger const randomNumber = yield* random.next return yield* logger.log(String(randomNumber)) }) // Combine service implementations into a single 'Context' const context = Context.empty().pipe( Context.add(Random, { next: Effect.sync(() => Math.random()) }), Context.add(Logger, { log: (message) => Effect.sync(() => console.log(message)) }) ) // Provide the entire context const runnable = Effect.provide(program, context) ``` ## Optional Services There are situations where we may want to access a service implementation only if it is available. In such cases, we can use the `Effect.serviceOption` function to handle this scenario. The `Effect.serviceOption` function returns an implementation that is available only if it is actually provided before executing this effect. To represent this optionality it returns an [Option](/docs/data-types/option/) of the implementation. **Example** (Handling Optional Services) To determine what action to take, we can use the `Option.isNone` function provided by the Option module. This function allows us to check if the service is available or not by returning `true` when the service is not available. ```ts twoslash import { Effect, Context, Option } from "effect" // Declaring a tag for a service that generates random numbers class Random extends Context.Tag("MyRandomService")< Random, { readonly next: Effect.Effect } >() {} const program = Effect.gen(function* () { const maybeRandom = yield* Effect.serviceOption(Random) const randomNumber = Option.isNone(maybeRandom) ? // the service is not available, return a default value -1 : // the service is available yield* maybeRandom.value.next console.log(randomNumber) }) ``` In the code above, we can observe that the `Requirements` type parameter of the `program` effect is `never`, even though we are working with a service. This allows us to access something from the context only if it is actually provided before executing this effect. When we run the `program` effect without providing the `Random` service: ```ts showLineNumbers=false Effect.runPromise(program).then(console.log) // Output: -1 ``` We see that the log message contains `-1`, which is the default value we provided when the service was not available. However, if we provide the `Random` service implementation: ```ts showLineNumbers=false Effect.runPromise( Effect.provideService(program, Random, { next: Effect.sync(() => Math.random()) }) ).then(console.log) // Example Output: 0.9957979486841035 ``` We can observe that the log message now contains a random number generated by the `next` operation of the `Random` service. ## Handling Services with Dependencies Sometimes a service in your application may depend on other services. To maintain a clean architecture, it's important to manage these dependencies without making them explicit in the service interface. Instead, you can use **layers** to handle these dependencies during the service construction phase. **Example** (Defining a Logger Service with a Configuration Dependency) Consider a scenario where multiple services depend on each other. In this case, the `Logger` service requires access to a configuration service (`Config`). ```ts twoslash import { Effect, Context } from "effect" // Declaring a tag for the Config service class Config extends Context.Tag("Config")() {} // Declaring a tag for the logging service class Logger extends Context.Tag("MyLoggerService")< Logger, { // ❌ Avoid exposing Config as a requirement readonly log: (message: string) => Effect.Effect } >() {} ``` To handle these dependencies in a structured way and prevent them from leaking into the service interfaces, you can use the `Layer` abstraction. For more details on managing dependencies with layers, refer to the [Managing Layers](/docs/requirements-management/layers/) page. # [Introduction](https://effect.website/docs/resource-management/introduction/) ## Overview In long-running applications, managing resources efficiently is essential, particularly when building large-scale systems. If resources like socket connections, database connections, or file descriptors are not properly managed, it can lead to resource leaks, which degrade application performance and reliability. Effect provides constructs that help ensure resources are properly managed and released, even in cases where exceptions occur. By ensuring that every time a resource is acquired, there is a corresponding mechanism to release it, Effect simplifies the process of resource management in your application. ## Finalization In many programming languages, the `try` / `finally` construct ensures that cleanup code runs regardless of whether an operation succeeds or fails. Effect provides similar functionality through `Effect.ensuring`, `Effect.onExit`, and `Effect.onError`. ### ensuring The `Effect.ensuring` function guarantees that a finalizer effect runs whether the main effect succeeds, fails, or is interrupted. This is useful for performing cleanup actions such as closing file handles, logging messages, or releasing locks. If you need access to the effect's result, consider using [onExit](#onexit). **Example** (Running a Finalizer in All Outcomes) ```ts twoslash import { Console, Effect } from "effect" // Define a cleanup effect const handler = Effect.ensuring(Console.log("Cleanup completed")) // Define a successful effect const success = Console.log("Task completed").pipe( Effect.as("some result"), handler ) Effect.runFork(success) /* Output: Task completed Cleanup completed */ // Define a failing effect const failure = Console.log("Task failed").pipe( Effect.andThen(Effect.fail("some error")), handler ) Effect.runFork(failure) /* Output: Task failed Cleanup completed */ // Define an interrupted effect const interruption = Console.log("Task interrupted").pipe( Effect.andThen(Effect.interrupt), handler ) Effect.runFork(interruption) /* Output: Task interrupted Cleanup completed */ ``` ### onExit `Effect.onExit` allows you to run a cleanup effect after the main effect completes, receiving an [Exit](/docs/data-types/exit/) value that describes the outcome. - If the effect succeeds, the `Exit` holds the success value. - If it fails, the `Exit` includes the error or failure cause. - If it is interrupted, the `Exit` reflects that interruption. The cleanup step itself is uninterruptible, which can help manage resources in complex or high-concurrency cases. **Example** (Running a Cleanup Function with the Effect's Result) ```ts twoslash import { Console, Effect, Exit } from "effect" // Define a cleanup effect that logs the result const handler = Effect.onExit((exit) => Console.log(`Cleanup completed: ${Exit.getOrElse(exit, String)}`) ) // Define a successful effect const success = Console.log("Task completed").pipe( Effect.as("some result"), handler ) Effect.runFork(success) /* Output: Task completed Cleanup completed: some result */ // Define a failing effect const failure = Console.log("Task failed").pipe( Effect.andThen(Effect.fail("some error")), handler ) Effect.runFork(failure) /* Output: Task failed Cleanup completed: Error: some error */ // Define an interrupted effect const interruption = Console.log("Task interrupted").pipe( Effect.andThen(Effect.interrupt), handler ) Effect.runFork(interruption) /* Output: Task interrupted Cleanup completed: All fibers interrupted without errors. */ ``` ### onError This function lets you attach a cleanup effect that runs whenever the calling effect fails, passing the cause of the failure to the cleanup effect. You can use it to perform actions such as logging, releasing resources, or applying additional recovery steps. The cleanup effect will also run if the failure is caused by interruption, and it is uninterruptible, so it always finishes once it starts. **Example** (Running Cleanup Only on Failure) ```ts twoslash import { Console, Effect } from "effect" // This handler logs the failure cause when the effect fails const handler = Effect.onError((cause) => Console.log(`Cleanup completed: ${cause}`) ) // Define a successful effect const success = Console.log("Task completed").pipe( Effect.as("some result"), handler ) Effect.runFork(success) /* Output: Task completed */ // Define a failing effect const failure = Console.log("Task failed").pipe( Effect.andThen(Effect.fail("some error")), handler ) Effect.runFork(failure) /* Output: Task failed Cleanup completed: Error: some error */ // Define a failing effect const defect = Console.log("Task failed with defect").pipe( Effect.andThen(Effect.die("Boom!")), handler ) Effect.runFork(defect) /* Output: Task failed with defect Cleanup completed: Error: Boom! */ // Define an interrupted effect const interruption = Console.log("Task interrupted").pipe( Effect.andThen(Effect.interrupt), handler ) Effect.runFork(interruption) /* Output: Task interrupted Cleanup completed: All fibers interrupted without errors. */ ``` ## acquireUseRelease Many real-world operations involve working with resources that must be released when no longer needed, such as: - Database connections - File handles - Network requests Effect provides `Effect.acquireUseRelease`, which ensures that a resource is: 1. **Acquired** properly. 2. **Used** for its intended purpose. 3. **Released** even if an error occurs. **Syntax** ```ts showLineNumbers=false Effect.acquireUseRelease(acquire, use, release) ``` **Example** (Automatically Managing Resource Lifetime) ```ts twoslash import { Effect, Console } from "effect" // Define an interface for a resource interface MyResource { readonly contents: string readonly close: () => Promise } // Simulate resource acquisition const getMyResource = (): Promise => Promise.resolve({ contents: "lorem ipsum", close: () => new Promise((resolve) => { console.log("Resource released") resolve() }) }) // Define how the resource is acquired const acquire = Effect.tryPromise({ try: () => getMyResource().then((res) => { console.log("Resource acquired") return res }), catch: () => new Error("getMyResourceError") }) // Define how the resource is released const release = (res: MyResource) => Effect.promise(() => res.close()) const use = (res: MyResource) => Console.log(`content is ${res.contents}`) // ┌─── Effect // ▼ const program = Effect.acquireUseRelease(acquire, use, release) Effect.runPromise(program) /* Output: Resource acquired content is lorem ipsum Resource released */ ``` # [Scope](https://effect.website/docs/resource-management/scope/) ## Overview import { Aside, Tabs, TabItem } from "@astrojs/starlight/components" The `Scope` data type is a core construct in Effect for managing resources in a safe and composable way. A scope represents the lifetime of one or more resources. When the scope is closed, all the resources within it are released, ensuring that no resources are leaked. Scopes also allow the addition of **finalizers**, which define how to release resources. With the `Scope` data type, you can: - **Add finalizers**: A finalizer specifies the cleanup logic for a resource. - **Close the scope**: When the scope is closed, all resources are released, and the finalizers are executed. **Example** (Managing a Scope) ```ts twoslash import { Scope, Effect, Console, Exit } from "effect" const program = // create a new scope Scope.make().pipe( // add finalizer 1 Effect.tap((scope) => Scope.addFinalizer(scope, Console.log("finalizer 1")) ), // add finalizer 2 Effect.tap((scope) => Scope.addFinalizer(scope, Console.log("finalizer 2")) ), // close the scope Effect.andThen((scope) => Scope.close(scope, Exit.succeed("scope closed successfully")) ) ) Effect.runPromise(program) /* Output: finalizer 2 <-- finalizers are closed in reverse order finalizer 1 */ ``` In the above example, finalizers are added to the scope, and when the scope is closed, the finalizers are **executed in the reverse order**. This reverse order is important because it ensures that resources are released in the correct sequence. For instance, if you acquire a network connection and then access a file on a remote server, the file must be closed before the network connection to avoid errors. ## addFinalizer The `Effect.addFinalizer` function is a high-level API that allows you to add finalizers to the scope of an effect. A finalizer is a piece of code that is guaranteed to run when the associated scope is closed. The behavior of the finalizer can vary based on the [Exit](/docs/data-types/exit/) value, which represents how the scope was closed—whether successfully or with an error. **Example** (Adding a Finalizer on Success) ```ts twoslash import { Effect, Console } from "effect" // ┌─── Effect // ▼ const program = Effect.gen(function* () { yield* Effect.addFinalizer((exit) => Console.log(`Finalizer executed. Exit status: ${exit._tag}`) ) return "some result" }) // Wrapping the effect in a scope // // ┌─── Effect // ▼ const runnable = Effect.scoped(program) Effect.runPromiseExit(runnable).then(console.log) /* Output: Finalizer executed. Exit status: Success { _id: 'Exit', _tag: 'Success', value: 'some result' } */ ``` ```ts twoslash import { Effect, Console } from "effect" // ┌─── Effect // ▼ const program = Effect.addFinalizer((exit) => Console.log(`Finalizer executed. Exit status: ${exit._tag}`) ).pipe(Effect.andThen(Effect.succeed("some result"))) // Wrapping the effect in a scope // // ┌─── Effect // ▼ const runnable = Effect.scoped(program) Effect.runPromiseExit(runnable).then(console.log) /* Output: Finalizer executed. Exit status: Success { _id: 'Exit', _tag: 'Success', value: 'some result' } */ ``` In this example, we use `Effect.addFinalizer` to add a finalizer that logs the exit state after the scope is closed. The finalizer will execute when the effect finishes, and it will log whether the effect completed successfully or failed. The type signature: ```ts showLineNumbers=false "Scope" const program: Effect ``` shows that the workflow requires a `Scope` to run. You can provide this `Scope` using the `Effect.scoped` function, which creates a new scope, runs the effect within it, and ensures the finalizers are executed when the scope is closed. **Example** (Adding a Finalizer on Failure) ```ts twoslash import { Effect, Console } from "effect" // ┌─── Effect // ▼ const program = Effect.gen(function* () { yield* Effect.addFinalizer((exit) => Console.log(`Finalizer executed. Exit status: ${exit._tag}`) ) return yield* Effect.fail("Uh oh!") }) // Wrapping the effect in a scope // // ┌─── Effect // ▼ const runnable = Effect.scoped(program) Effect.runPromiseExit(runnable).then(console.log) /* Output: Finalizer executed. Exit status: Failure { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Uh oh!' } } */ ``` ```ts twoslash import { Effect, Console } from "effect" // ┌─── Effect // ▼ const program = Effect.addFinalizer((exit) => Console.log(`Finalizer executed. Exit status: ${exit._tag}`) ).pipe(Effect.andThen(Effect.fail("Uh oh!"))) // Wrapping the effect in a scope // // ┌─── Effect // ▼ const runnable = Effect.scoped(program) Effect.runPromiseExit(runnable).then(console.log) /* Output: Finalizer executed. Exit status: Failure { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Uh oh!' } } */ ``` In this case, the finalizer is executed even when the effect fails. The log output reflects that the finalizer runs after the failure, and it logs the failure details. **Example** (Adding a Finalizer on [Interruption](/docs/concurrency/basic-concurrency/#interruptions)) ```ts twoslash import { Effect, Console } from "effect" // ┌─── Effect // ▼ const program = Effect.gen(function* () { yield* Effect.addFinalizer((exit) => Console.log(`Finalizer executed. Exit status: ${exit._tag}`) ) return yield* Effect.interrupt }) // Wrapping the effect in a scope // // ┌─── Effect // ▼ const runnable = Effect.scoped(program) Effect.runPromiseExit(runnable).then(console.log) /* Output: Finalizer executed. Exit status: Failure { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Interrupt', fiberId: { _id: 'FiberId', _tag: 'Runtime', id: 0, startTimeMillis: ... } } } */ ``` ```ts twoslash import { Effect, Console } from "effect" // ┌─── Effect // ▼ const program = Effect.addFinalizer((exit) => Console.log(`Finalizer executed. Exit status: ${exit._tag}`) ).pipe(Effect.andThen(Effect.interrupt)) // Wrapping the effect in a scope // // ┌─── Effect // ▼ const runnable = Effect.scoped(program) Effect.runPromiseExit(runnable).then(console.log) /* Output: Finalizer executed. Exit status: Failure { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Interrupt', fiberId: { _id: 'FiberId', _tag: 'Runtime', id: 0, startTimeMillis: ... } } } */ ``` This example shows how a finalizer behaves when the effect is interrupted. The finalizer runs after the interruption, and the exit status reflects that the effect was stopped mid-execution. ## Manually Create and Close Scopes When you're working with multiple scoped resources within a single operation, it's important to understand how their scopes interact. By default, these scopes are merged into one, but you can have more fine-grained control over when each scope is closed by manually creating and closing them. Let's start by looking at how scopes are merged by default: **Example** (Merging Scopes) ```ts twoslash import { Effect, Console } from "effect" const task1 = Effect.gen(function* () { console.log("task 1") yield* Effect.addFinalizer(() => Console.log("finalizer after task 1")) }) const task2 = Effect.gen(function* () { console.log("task 2") yield* Effect.addFinalizer(() => Console.log("finalizer after task 2")) }) const program = Effect.gen(function* () { // The scopes of both tasks are merged into one yield* task1 yield* task2 }) Effect.runPromise(Effect.scoped(program)) /* Output: task 1 task 2 finalizer after task 2 finalizer after task 1 */ ``` In this case, the scopes of `task1` and `task2` are merged into a single scope, and when the program is run, it outputs the tasks and their finalizers in a specific order. If you want more control over when each scope is closed, you can manually create and close them: **Example** (Manually Creating and Closing Scopes) ```ts twoslash import { Console, Effect, Exit, Scope } from "effect" const task1 = Effect.gen(function* () { console.log("task 1") yield* Effect.addFinalizer(() => Console.log("finalizer after task 1")) }) const task2 = Effect.gen(function* () { console.log("task 2") yield* Effect.addFinalizer(() => Console.log("finalizer after task 2")) }) const program = Effect.gen(function* () { const scope1 = yield* Scope.make() const scope2 = yield* Scope.make() // Extend the scope of task1 into scope1 yield* task1.pipe(Scope.extend(scope1)) // Extend the scope of task2 into scope2 yield* task2.pipe(Scope.extend(scope2)) // Manually close scope1 and scope2 yield* Scope.close(scope1, Exit.void) yield* Console.log("doing something else") yield* Scope.close(scope2, Exit.void) }) Effect.runPromise(program) /* Output: task 1 task 2 finalizer after task 1 doing something else finalizer after task 2 */ ``` In this example, we create two separate scopes, `scope1` and `scope2`, and extend the scope of each task into its respective scope. When you run the program, it outputs the tasks and their finalizers in a different order. You might wonder what happens when a scope is closed, but a task within that scope hasn't completed yet. The key point to note is that the scope closing doesn't force the task to be interrupted. **Example** (Closing a Scope with Pending Tasks) ```ts twoslash import { Console, Effect, Exit, Scope } from "effect" const task = Effect.gen(function* () { yield* Effect.sleep("1 second") console.log("Executed") yield* Effect.addFinalizer(() => Console.log("Task Finalizer")) }) const program = Effect.gen(function* () { const scope = yield* Scope.make() // Close the scope immediately yield* Scope.close(scope, Exit.void) console.log("Scope closed") // This task will be executed even if the scope is closed yield* task.pipe(Scope.extend(scope)) }) Effect.runPromise(program) /* Output: Scope closed Executed <-- after 1 second Task Finalizer */ ``` ## Defining Resources ### acquireRelease The `Effect.acquireRelease(acquire, release)` function allows you to define resources that are acquired and safely released when they are no longer needed. This is useful for managing resources such as file handles, database connections, or network sockets. To use `Effect.acquireRelease`, you need to define two actions: 1. **Acquiring the Resource**: An effect describing the acquisition of the resource, e.g., opening a file or establishing a database connection. 2. **Releasing the Resource**: The clean-up effect that ensures the resource is properly released, e.g., closing the file or the connection. The acquisition process is **uninterruptible** to ensure that partial resource acquisition doesn't leave your system in an inconsistent state. The `Effect.acquireRelease` function guarantees that once a resource is successfully acquired, its release step is always executed when the `Scope` is closed. **Example** (Defining a Simple Resource) ```ts twoslash import { Effect } from "effect" // Define an interface for a resource interface MyResource { readonly contents: string readonly close: () => Promise } // Simulate resource acquisition const getMyResource = (): Promise => Promise.resolve({ contents: "lorem ipsum", close: () => new Promise((resolve) => { console.log("Resource released") resolve() }) }) // Define how the resource is acquired const acquire = Effect.tryPromise({ try: () => getMyResource().then((res) => { console.log("Resource acquired") return res }), catch: () => new Error("getMyResourceError") }) // Define how the resource is released const release = (res: MyResource) => Effect.promise(() => res.close()) // Create the resource management workflow // // ┌─── Effect // ▼ const resource = Effect.acquireRelease(acquire, release) ``` In the code above, the `Effect.acquireRelease` function creates a resource workflow that requires a `Scope`: ```ts showLineNumbers=false "Scope" const resource: Effect ``` This means that the workflow needs a `Scope` to run, and the resource will automatically be released when the scope is closed. You can now use the resource by chaining operations using `Effect.andThen` or similar functions. We can continue working with the resource for as long as we want by using `Effect.andThen` or other Effect operators. For example, here's how we can read the contents: **Example** (Using the Resource) ```ts twoslash collapse={3-34} import { Effect } from "effect" // Define an interface for a resource interface MyResource { readonly contents: string readonly close: () => Promise } // Simulate resource acquisition const getMyResource = (): Promise => Promise.resolve({ contents: "lorem ipsum", close: () => new Promise((resolve) => { console.log("Resource released") resolve() }) }) // Define how the resource is acquired const acquire = Effect.tryPromise({ try: () => getMyResource().then((res) => { console.log("Resource acquired") return res }), catch: () => new Error("getMyResourceError") }) // Define how the resource is released const release = (res: MyResource) => Effect.promise(() => res.close()) // Create the resource management workflow const resource = Effect.acquireRelease(acquire, release) // ┌─── Effect // ▼ const program = Effect.gen(function* () { const res = yield* resource console.log(`content is ${res.contents}`) }) ``` To ensure proper resource management, the `Scope` should be closed when you're done with the resource. The `Effect.scoped` function handles this for you by creating a `Scope`, running the effect, and then closing the `Scope` when the effect finishes. **Example** (Providing the `Scope` with `Effect.scoped`) ```ts twoslash collapse={3-34} import { Effect } from "effect" // Define an interface for a resource interface MyResource { readonly contents: string readonly close: () => Promise } // Simulate resource acquisition const getMyResource = (): Promise => Promise.resolve({ contents: "lorem ipsum", close: () => new Promise((resolve) => { console.log("Resource released") resolve() }) }) // Define how the resource is acquired const acquire = Effect.tryPromise({ try: () => getMyResource().then((res) => { console.log("Resource acquired") return res }), catch: () => new Error("getMyResourceError") }) // Define how the resource is released const release = (res: MyResource) => Effect.promise(() => res.close()) // Create the resource management workflow const resource = Effect.acquireRelease(acquire, release) // ┌─── Effect // ▼ const program = Effect.scoped( Effect.gen(function* () { const res = yield* resource console.log(`content is ${res.contents}`) }) ) // We now have a workflow that is ready to run Effect.runPromise(program) /* Resource acquired content is lorem ipsum Resource released */ ``` ### Example Pattern: Sequencing Operations In certain scenarios, you might need to perform a sequence of chained operations where the success of each operation depends on the previous one. However, if any of the operations fail, you would want to reverse the effects of all previous successful operations. This pattern is valuable when you need to ensure that either all operations succeed, or none of them have any effect at all. Let's go through an example of implementing this pattern. Suppose we want to create a "Workspace" in our application, which involves creating an S3 bucket, an ElasticSearch index, and a Database entry that relies on the previous two. To begin, we define the domain model for the required [services](/docs/requirements-management/services/): - `S3` - `ElasticSearch` - `Database` ```ts twoslash import { Effect, Context } from "effect" class S3Error { readonly _tag = "S3Error" } interface Bucket { readonly name: string } class S3 extends Context.Tag("S3")< S3, { readonly createBucket: Effect.Effect readonly deleteBucket: (bucket: Bucket) => Effect.Effect } >() {} class ElasticSearchError { readonly _tag = "ElasticSearchError" } interface Index { readonly id: string } class ElasticSearch extends Context.Tag("ElasticSearch")< ElasticSearch, { readonly createIndex: Effect.Effect readonly deleteIndex: (index: Index) => Effect.Effect } >() {} class DatabaseError { readonly _tag = "DatabaseError" } interface Entry { readonly id: string } class Database extends Context.Tag("Database")< Database, { readonly createEntry: ( bucket: Bucket, index: Index ) => Effect.Effect readonly deleteEntry: (entry: Entry) => Effect.Effect } >() {} ``` Next, we define the three create actions and the overall transaction (`make`) for the ```ts twoslash collapse={3-52} import { Effect, Context, Exit } from "effect" class S3Error { readonly _tag = "S3Error" } interface Bucket { readonly name: string } class S3 extends Context.Tag("S3")< S3, { readonly createBucket: Effect.Effect readonly deleteBucket: (bucket: Bucket) => Effect.Effect } >() {} class ElasticSearchError { readonly _tag = "ElasticSearchError" } interface Index { readonly id: string } class ElasticSearch extends Context.Tag("ElasticSearch")< ElasticSearch, { readonly createIndex: Effect.Effect readonly deleteIndex: (index: Index) => Effect.Effect } >() {} class DatabaseError { readonly _tag = "DatabaseError" } interface Entry { readonly id: string } class Database extends Context.Tag("Database")< Database, { readonly createEntry: ( bucket: Bucket, index: Index ) => Effect.Effect readonly deleteEntry: (entry: Entry) => Effect.Effect } >() {} // Create a bucket, and define the release function that deletes the // bucket if the operation fails. const createBucket = Effect.gen(function* () { const { createBucket, deleteBucket } = yield* S3 return yield* Effect.acquireRelease(createBucket, (bucket, exit) => // The release function for the Effect.acquireRelease operation is // responsible for handling the acquired resource (bucket) after the // main effect has completed. It is called regardless of whether the // main effect succeeded or failed. If the main effect failed, // Exit.isFailure(exit) will be true, and the function will perform // a rollback by calling deleteBucket(bucket). If the main effect // succeeded, Exit.isFailure(exit) will be false, and the function // will return Effect.void, representing a successful, but // do-nothing effect. Exit.isFailure(exit) ? deleteBucket(bucket) : Effect.void ) }) // Create an index, and define the release function that deletes the // index if the operation fails. const createIndex = Effect.gen(function* () { const { createIndex, deleteIndex } = yield* ElasticSearch return yield* Effect.acquireRelease(createIndex, (index, exit) => Exit.isFailure(exit) ? deleteIndex(index) : Effect.void ) }) // Create an entry in the database, and define the release function that // deletes the entry if the operation fails. const createEntry = (bucket: Bucket, index: Index) => Effect.gen(function* () { const { createEntry, deleteEntry } = yield* Database return yield* Effect.acquireRelease( createEntry(bucket, index), (entry, exit) => Exit.isFailure(exit) ? deleteEntry(entry) : Effect.void ) }) const make = Effect.scoped( Effect.gen(function* () { const bucket = yield* createBucket const index = yield* createIndex return yield* createEntry(bucket, index) }) ) ``` We then create simple service implementations to test the behavior of our Workspace code. To achieve this, we will utilize [layers](/docs/requirements-management/layers/) to construct test These layers will be able to handle various scenarios, including errors, which we can control using the `FailureCase` type. ```ts twoslash collapse={3-99} import { Effect, Context, Layer, Console, Exit } from "effect" class S3Error { readonly _tag = "S3Error" } interface Bucket { readonly name: string } class S3 extends Context.Tag("S3")< S3, { readonly createBucket: Effect.Effect readonly deleteBucket: (bucket: Bucket) => Effect.Effect } >() {} class ElasticSearchError { readonly _tag = "ElasticSearchError" } interface Index { readonly id: string } class ElasticSearch extends Context.Tag("ElasticSearch")< ElasticSearch, { readonly createIndex: Effect.Effect readonly deleteIndex: (index: Index) => Effect.Effect } >() {} class DatabaseError { readonly _tag = "DatabaseError" } interface Entry { readonly id: string } class Database extends Context.Tag("Database")< Database, { readonly createEntry: ( bucket: Bucket, index: Index ) => Effect.Effect readonly deleteEntry: (entry: Entry) => Effect.Effect } >() {} // Create a bucket, and define the release function that deletes the // bucket if the operation fails. const createBucket = Effect.gen(function* () { const { createBucket, deleteBucket } = yield* S3 return yield* Effect.acquireRelease(createBucket, (bucket, exit) => // The release function for the Effect.acquireRelease operation is // responsible for handling the acquired resource (bucket) after the // main effect has completed. It is called regardless of whether the // main effect succeeded or failed. If the main effect failed, // Exit.isFailure(exit) will be true, and the function will perform // a rollback by calling deleteBucket(bucket). If the main effect // succeeded, Exit.isFailure(exit) will be false, and the function // will return Effect.void, representing a successful, but // do-nothing effect. Exit.isFailure(exit) ? deleteBucket(bucket) : Effect.void ) }) // Create an index, and define the release function that deletes the // index if the operation fails. const createIndex = Effect.gen(function* () { const { createIndex, deleteIndex } = yield* ElasticSearch return yield* Effect.acquireRelease(createIndex, (index, exit) => Exit.isFailure(exit) ? deleteIndex(index) : Effect.void ) }) // Create an entry in the database, and define the release function that // deletes the entry if the operation fails. const createEntry = (bucket: Bucket, index: Index) => Effect.gen(function* () { const { createEntry, deleteEntry } = yield* Database return yield* Effect.acquireRelease( createEntry(bucket, index), (entry, exit) => Exit.isFailure(exit) ? deleteEntry(entry) : Effect.void ) }) const make = Effect.scoped( Effect.gen(function* () { const bucket = yield* createBucket const index = yield* createIndex return yield* createEntry(bucket, index) }) ) // The `FailureCaseLiterals` type allows us to provide different error // scenarios while testing our // // For example, by providing the value "S3", we can simulate an error // scenario specific to the S3 service. This helps us ensure that our // program handles errors correctly and behaves as expected in various // situations. // // Similarly, we can provide other values like "ElasticSearch" or // "Database" to simulate error scenarios for those In cases // where we want to test the absence of errors, we can provide // `undefined`. By using this parameter, we can thoroughly test our // services and verify their behavior under different error conditions. type FailureCaseLiterals = "S3" | "ElasticSearch" | "Database" | undefined class FailureCase extends Context.Tag("FailureCase")< FailureCase, FailureCaseLiterals >() {} // Create a test layer for the S3 service const S3Test = Layer.effect( S3, Effect.gen(function* () { const failureCase = yield* FailureCase return { createBucket: Effect.gen(function* () { console.log("[S3] creating bucket") if (failureCase === "S3") { return yield* Effect.fail(new S3Error()) } else { return { name: "" } } }), deleteBucket: (bucket) => Console.log(`[S3] delete bucket ${bucket.name}`) } }) ) // Create a test layer for the ElasticSearch service const ElasticSearchTest = Layer.effect( ElasticSearch, Effect.gen(function* () { const failureCase = yield* FailureCase return { createIndex: Effect.gen(function* () { console.log("[ElasticSearch] creating index") if (failureCase === "ElasticSearch") { return yield* Effect.fail(new ElasticSearchError()) } else { return { id: "" } } }), deleteIndex: (index) => Console.log(`[ElasticSearch] delete index ${index.id}`) } }) ) // Create a test layer for the Database service const DatabaseTest = Layer.effect( Database, Effect.gen(function* () { const failureCase = yield* FailureCase return { createEntry: (bucket, index) => Effect.gen(function* () { console.log( "[Database] creating entry for bucket" + `${bucket.name} and index ${index.id}` ) if (failureCase === "Database") { return yield* Effect.fail(new DatabaseError()) } else { return { id: "" } } }), deleteEntry: (entry) => Console.log(`[Database] delete entry ${entry.id}`) } }) ) // Merge all the test layers for S3, ElasticSearch, and Database // services into a single layer const layer = Layer.mergeAll(S3Test, ElasticSearchTest, DatabaseTest) // Create a runnable effect to test the Workspace code. The effect is // provided with the test layer and a FailureCase service with undefined // value (no failure case). const runnable = make.pipe( Effect.provide(layer), Effect.provideService(FailureCase, undefined) ) Effect.runPromise(Effect.either(runnable)).then(console.log) ``` Let's examine the test results for the scenario where `FailureCase` is set to `undefined` (happy path): ```ansi showLineNumbers=false [S3] creating bucket [ElasticSearch] creating index [Database] creating entry for bucket and index { _id: "Either", _tag: "Right", right: { id: "" } } ``` In this case, all operations succeed, and we see a successful result with `right({ id: '' })`. Now, let's simulate a failure in the `Database`: ```ts showLineNumbers=false const runnable = make.pipe( Effect.provide(layer), Effect.provideService(FailureCase, "Database") ) ``` The console output will be: ```ansi showLineNumbers=false [S3] creating bucket [ElasticSearch] creating index [Database] creating entry for bucket and index [ElasticSearch] delete index [S3] delete bucket { _id: "Either", _tag: "Left", left: { _tag: "DatabaseError" } } ``` You can observe that once the `Database` error occurs, there is a complete rollback that deletes the `ElasticSearch` index first and then the associated `S3` bucket. The result is a failure with `left(new DatabaseError())`. Let's now make the index creation fail instead: ```ts showLineNumbers=false const runnable = make.pipe( Effect.provide(layer), Effect.provideService(FailureCase, "ElasticSearch") ) ``` In this case, the console output will be: ```ansi showLineNumbers=false [S3] creating bucket [ElasticSearch] creating index [S3] delete bucket { _id: "Either", _tag: "Left", left: { _tag: "ElasticSearchError" } } ``` As expected, once the `ElasticSearch` index creation fails, there is a rollback that deletes the `S3` bucket. The result is a failure with `left(new ElasticSearchError())`. # [Built-In Schedules](https://effect.website/docs/scheduling/built-in-schedules/) ## Overview import { Aside } from "@astrojs/starlight/components" To demonstrate the functionality of different schedules, we will use the following helper function that logs each repetition along with the corresponding delay in milliseconds, formatted as: ```text showLineNumbers=false #: ``` **Helper** (Logging Execution Delays) ```ts twoslash import { Array, Chunk, Duration, Effect, Schedule } from "effect" const log = ( schedule: Schedule.Schedule, delay: Duration.DurationInput = 0 ): void => { const maxRecurs = 10 // Limit the number of executions const delays = Chunk.toArray( Effect.runSync( Schedule.run( Schedule.delays(Schedule.addDelay(schedule, () => delay)), Date.now(), Array.range(0, maxRecurs) ) ) ) delays.forEach((duration, i) => { console.log( i === maxRecurs ? "..." // Indicate truncation if there are more executions : i === delays.length - 1 ? "(end)" // Mark the last execution : `#${i + 1}: ${Duration.toMillis(duration)}ms` ) }) } ``` ## Infinite and Fixed Repeats ### forever A schedule that repeats indefinitely, producing the number of recurrences each time it runs. **Example** (Indefinitely Recurring Schedule) ```ts twoslash collapse={3-26} import { Array, Chunk, Duration, Effect, Schedule } from "effect" const log = ( schedule: Schedule.Schedule, delay: Duration.DurationInput = 0 ): void => { const maxRecurs = 10 const delays = Chunk.toArray( Effect.runSync( Schedule.run( Schedule.delays(Schedule.addDelay(schedule, () => delay)), Date.now(), Array.range(0, maxRecurs) ) ) ) delays.forEach((duration, i) => { console.log( i === maxRecurs ? "..." : i === delays.length - 1 ? "(end)" : `#${i + 1}: ${Duration.toMillis(duration)}ms` ) }) } const schedule = Schedule.forever log(schedule) /* Output: #1: 0ms < forever #2: 0ms #3: 0ms #4: 0ms #5: 0ms #6: 0ms #7: 0ms #8: 0ms #9: 0ms #10: 0ms ... */ ``` ### once A schedule that recurs only once. **Example** (Single Recurrence Schedule) ```ts twoslash collapse={3-26} import { Array, Chunk, Duration, Effect, Schedule } from "effect" const log = ( schedule: Schedule.Schedule, delay: Duration.DurationInput = 0 ): void => { const maxRecurs = 10 const delays = Chunk.toArray( Effect.runSync( Schedule.run( Schedule.delays(Schedule.addDelay(schedule, () => delay)), Date.now(), Array.range(0, maxRecurs) ) ) ) delays.forEach((duration, i) => { console.log( i === maxRecurs ? "..." : i === delays.length - 1 ? "(end)" : `#${i + 1}: ${Duration.toMillis(duration)}ms` ) }) } const schedule = Schedule.once log(schedule) /* Output: #1: 0ms < once (end) */ ``` ### recurs A schedule that repeats a specified number of times, producing the number of recurrences each time it runs. **Example** (Fixed Number of Recurrences) ```ts twoslash collapse={3-26} import { Array, Chunk, Duration, Effect, Schedule } from "effect" const log = ( schedule: Schedule.Schedule, delay: Duration.DurationInput = 0 ): void => { const maxRecurs = 10 const delays = Chunk.toArray( Effect.runSync( Schedule.run( Schedule.delays(Schedule.addDelay(schedule, () => delay)), Date.now(), Array.range(0, maxRecurs) ) ) ) delays.forEach((duration, i) => { console.log( i === maxRecurs ? "..." : i === delays.length - 1 ? "(end)" : `#${i + 1}: ${Duration.toMillis(duration)}ms` ) }) } const schedule = Schedule.recurs(5) log(schedule) /* Output: #1: 0ms < recurs #2: 0ms #3: 0ms #4: 0ms #5: 0ms (end) */ ``` ## Recurring at specific intervals You can define schedules that control the time between executions. The difference between `spaced` and `fixed` schedules lies in how the interval is measured: - `spaced` delays each repetition from the **end** of the previous one. - `fixed` ensures repetitions occur at **regular intervals**, regardless of execution time. ### spaced A schedule that repeats indefinitely, each repetition spaced the specified duration from the last run. It returns the number of recurrences each time it runs. **Example** (Recurring with Delay Between Executions) ```ts twoslash collapse={3-26} import { Array, Chunk, Duration, Effect, Schedule } from "effect" const log = ( schedule: Schedule.Schedule, delay: Duration.DurationInput = 0 ): void => { const maxRecurs = 10 const delays = Chunk.toArray( Effect.runSync( Schedule.run( Schedule.delays(Schedule.addDelay(schedule, () => delay)), Date.now(), Array.range(0, maxRecurs) ) ) ) delays.forEach((duration, i) => { console.log( i === maxRecurs ? "..." : i === delays.length - 1 ? "(end)" : `#${i + 1}: ${Duration.toMillis(duration)}ms` ) }) } const schedule = Schedule.spaced("200 millis") // ┌─── Simulating an effect that takes // │ 100 milliseconds to complete // ▼ log(schedule, "100 millis") /* Output: #1: 300ms < spaced #2: 300ms #3: 300ms #4: 300ms #5: 300ms #6: 300ms #7: 300ms #8: 300ms #9: 300ms #10: 300ms ... */ ``` The first delay is approximately 100 milliseconds, as the initial execution is not affected by the schedule. Subsequent delays are approximately 200 milliseconds apart, demonstrating the effect of the `spaced` schedule. ### fixed A schedule that recurs at fixed intervals. It returns the number of recurrences each time it runs. If the action run between updates takes longer than the interval, then the action will be run immediately, but re-runs will not "pile up". ```text showLineNumbers=false |-----interval-----|-----interval-----|-----interval-----| |---------action--------|action-------|action------------| ``` **Example** (Fixed Interval Recurrence) ```ts twoslash collapse={3-26} import { Array, Chunk, Duration, Effect, Schedule } from "effect" const log = ( schedule: Schedule.Schedule, delay: Duration.DurationInput = 0 ): void => { const maxRecurs = 10 const delays = Chunk.toArray( Effect.runSync( Schedule.run( Schedule.delays(Schedule.addDelay(schedule, () => delay)), Date.now(), Array.range(0, maxRecurs) ) ) ) delays.forEach((duration, i) => { console.log( i === maxRecurs ? "..." : i === delays.length - 1 ? "(end)" : `#${i + 1}: ${Duration.toMillis(duration)}ms` ) }) } const schedule = Schedule.fixed("200 millis") // ┌─── Simulating an effect that takes // │ 100 milliseconds to complete // ▼ log(schedule, "100 millis") /* Output: #1: 300ms < fixed #2: 200ms #3: 200ms #4: 200ms #5: 200ms #6: 200ms #7: 200ms #8: 200ms #9: 200ms #10: 200ms ... */ ``` ## Increasing Delays Between Executions ### exponential A schedule that recurs using exponential backoff, with each delay increasing exponentially. Returns the current duration between recurrences. **Example** (Exponential Backoff Schedule) ```ts twoslash collapse={3-26} import { Array, Chunk, Duration, Effect, Schedule } from "effect" const log = ( schedule: Schedule.Schedule, delay: Duration.DurationInput = 0 ): void => { const maxRecurs = 10 const delays = Chunk.toArray( Effect.runSync( Schedule.run( Schedule.delays(Schedule.addDelay(schedule, () => delay)), Date.now(), Array.range(0, maxRecurs) ) ) ) delays.forEach((duration, i) => { console.log( i === maxRecurs ? "..." : i === delays.length - 1 ? "(end)" : `#${i + 1}: ${Duration.toMillis(duration)}ms` ) }) } const schedule = Schedule.exponential("10 millis") log(schedule) /* Output: #1: 10ms < exponential #2: 20ms #3: 40ms #4: 80ms #5: 160ms #6: 320ms #7: 640ms #8: 1280ms #9: 2560ms #10: 5120ms ... */ ``` ### fibonacci A schedule that always recurs, increasing delays by summing the preceding two delays (similar to the fibonacci sequence). Returns the current duration between recurrences. **Example** (Fibonacci Delay Schedule) ```ts twoslash collapse={3-26} import { Array, Chunk, Duration, Effect, Schedule } from "effect" const log = ( schedule: Schedule.Schedule, delay: Duration.DurationInput = 0 ): void => { const maxRecurs = 10 const delays = Chunk.toArray( Effect.runSync( Schedule.run( Schedule.delays(Schedule.addDelay(schedule, () => delay)), Date.now(), Array.range(0, maxRecurs) ) ) ) delays.forEach((duration, i) => { console.log( i === maxRecurs ? "..." : i === delays.length - 1 ? "(end)" : `#${i + 1}: ${Duration.toMillis(duration)}ms` ) }) } const schedule = Schedule.fibonacci("10 millis") log(schedule) /* Output: #1: 10ms < fibonacci #2: 10ms #3: 20ms #4: 30ms #5: 50ms #6: 80ms #7: 130ms #8: 210ms #9: 340ms #10: 550ms ... */ ``` # [Cron](https://effect.website/docs/scheduling/cron/) ## Overview import { Aside } from "@astrojs/starlight/components" The Cron module lets you define schedules in a style similar to [UNIX cron expressions](https://en.wikipedia.org/wiki/Cron). It also supports partial constraints (e.g., certain months or weekdays), time zone awareness through the [DateTime](/docs/data-types/datetime/) module, and robust error handling. This module helps you: - **Create** a `Cron` instance from individual parts. - **Parse and validate** cron expressions. - **Match** existing dates to see if they satisfy a given cron schedule. - **Find** the next occurrence of a schedule after a given date. - **Iterate** over future dates that match a schedule. - **Convert** a `Cron` instance to a `Schedule` for use in effectful programs. ## Creating a Cron You can define a cron schedule by specifying numeric constraints for seconds, minutes, hours, days, months, and weekdays. The `make` function requires you to define all fields representing the schedule's constraints. **Example** (Creating a Cron) ```ts twoslash import { Cron, DateTime } from "effect" // Build a cron that triggers at 4:00 AM // on the 8th to the 14th of each month const cron = Cron.make({ seconds: [0], // Trigger at the start of a minute minutes: [0], // Trigger at the start of an hour hours: [4], // Trigger at 4:00 AM days: [8, 9, 10, 11, 12, 13, 14], // Specific days of the month months: [], // No restrictions on the month weekdays: [], // No restrictions on the weekday tz: DateTime.zoneUnsafeMakeNamed("Europe/Rome") // Optional time zone }) ``` - `seconds`, `minutes`, and `hours`: Define the time of day. - `days` and `months`: Specify which calendar days and months are valid. - `weekdays`: Restrict the schedule to specific days of the week. - `tz`: Optionally define the time zone for the schedule. If any field is left empty (e.g., `months`), it is treated as having "no constraints," allowing any valid value for that part of the date. ## Parsing Cron Expressions Instead of manually constructing a `Cron`, you can use UNIX-like cron strings and parse them with `parse` or `unsafeParse`. ### parse The `parse(cronExpression, tz?)` function safely parses a cron string into a `Cron` instance. It returns an [Either](/docs/data-types/either/), which will contain either the parsed `Cron` or a parsing error. **Example** (Safely Parsing a Cron Expression) ```ts twoslash import { Either, Cron } from "effect" // Define a cron expression for 4:00 AM // on the 8th to the 14th of every month const expression = "0 0 4 8-14 * *" // Parse the cron expression const eitherCron = Cron.parse(expression) if (Either.isRight(eitherCron)) { // Successfully parsed console.log("Parsed cron:", eitherCron.right) } else { // Parsing failed console.error("Failed to parse cron:", eitherCron.left.message) } ``` ### unsafeParse The `unsafeParse(cronExpression, tz?)` function works like [parse](#parse), but instead of returning an [Either](/docs/data-types/either/), it throws an exception if the input is invalid. **Example** (Parsing a Cron Expression) ```ts twoslash import { Cron } from "effect" // Parse a cron expression for 4:00 AM // on the 8th to the 14th of every month // Throws if the expression is invalid const cron = Cron.unsafeParse("0 0 4 8-14 * *") ``` ## Checking Dates with match The `match` function allows you to determine if a given `Date` (or any [DateTime.Input](/docs/data-types/datetime/#the-datetimeinput-type)) satisfies the constraints of a cron schedule. If the date meets the schedule's conditions, `match` returns `true`. Otherwise, it returns `false`. **Example** (Checking if a Date Matches a Cron Schedule) ```ts twoslash import { Cron } from "effect" // Suppose we have a cron that triggers at 4:00 AM // on the 8th to the 14th of each month const cron = Cron.unsafeParse("0 0 4 8-14 * *") const checkDate = new Date("2025-01-08 04:00:00") console.log(Cron.match(cron, checkDate)) // Output: true ``` ## Finding the Next Run The `next` function determines the next date that satisfies a given cron schedule, starting from a specified date. If no starting date is provided, the current time is used as the starting point. If `next` cannot find a matching date within a predefined number of iterations, it throws an error to prevent infinite loops. **Example** (Determining the Next Matching Date) ```ts twoslash import { Cron } from "effect" // Define a cron expression for 4:00 AM // on the 8th to the 14th of every month const cron = Cron.unsafeParse("0 0 4 8-14 * *", "UTC") // Specify the starting point for the search const after = new Date("2025-01-08") // Find the next matching date const nextDate = Cron.next(cron, after) console.log(nextDate) // Output: 2025-01-08T04:00:00.000Z ``` ## Iterating Over Future Dates To generate multiple future dates that match a cron schedule, you can use the `sequence` function. This function provides an infinite iterator of matching dates, starting from a specified date. **Example** (Generating Future Dates with an Iterator) ```ts import { Cron } from "effect" // Define a cron expression for 4:00 AM // on the 8th to the 14th of every month const cron = Cron.unsafeParse("0 0 4 8-14 * *", "UTC") // Specify the starting date const start = new Date("2021-01-08") // Create an iterator for the schedule const iterator = Cron.sequence(cron, start) // Get the first matching date after the start date console.log(iterator.next().value) // Output: 2021-01-08T04:00:00.000Z // Get the second matching date after the start date console.log(iterator.next().value) // Output: 2021-01-09T04:00:00.000Z ``` ## Converting to Schedule The Schedule module allows you to define recurring behaviors, such as retries or periodic events. The `cron` function bridges the `Cron` module with the Schedule module, enabling you to create schedules based on cron expressions or `Cron` instances. ### cron The `Schedule.cron` function generates a [Schedule](/docs/scheduling/introduction/) that triggers at the start of each interval defined by the provided cron expression or `Cron` instance. When triggered, the schedule produces a tuple `[start, end]` representing the timestamps (in milliseconds) of the cron interval window. **Example** (Creating a Schedule from a Cron) ```ts twoslash collapse={12-40} import { Effect, Schedule, TestClock, Fiber, TestContext, Cron, Console } from "effect" // A helper function to log output at each interval of the schedule const log = ( action: Effect.Effect, schedule: Schedule.Schedule<[number, number], void> ): void => { let i = 0 Effect.gen(function* () { const fiber: Fiber.RuntimeFiber<[[number, number], number]> = yield* Effect.gen(function* () { yield* action i++ }).pipe( Effect.repeat( schedule.pipe( // Limit the number of iterations for the example Schedule.intersect(Schedule.recurs(10)), Schedule.tapOutput(([Out]) => Console.log( i === 11 ? "..." : [new Date(Out[0]), new Date(Out[1])] ) ) ) ), Effect.fork ) yield* TestClock.adjust(Infinity) yield* Fiber.join(fiber) }).pipe(Effect.provide(TestContext.TestContext), Effect.runPromise) } // Build a cron that triggers at 4:00 AM // on the 8th to the 14th of each month const cron = Cron.unsafeParse("0 0 4 8-14 * *", "UTC") // Convert the Cron into a Schedule const schedule = Schedule.cron(cron) // Define a dummy action to repeat const action = Effect.void // Log the schedule intervals log(action, schedule) /* Output: [ 1970-01-08T04:00:00.000Z, 1970-01-08T04:00:01.000Z ] [ 1970-01-09T04:00:00.000Z, 1970-01-09T04:00:01.000Z ] [ 1970-01-10T04:00:00.000Z, 1970-01-10T04:00:01.000Z ] [ 1970-01-11T04:00:00.000Z, 1970-01-11T04:00:01.000Z ] [ 1970-01-12T04:00:00.000Z, 1970-01-12T04:00:01.000Z ] [ 1970-01-13T04:00:00.000Z, 1970-01-13T04:00:01.000Z ] [ 1970-01-14T04:00:00.000Z, 1970-01-14T04:00:01.000Z ] [ 1970-02-08T04:00:00.000Z, 1970-02-08T04:00:01.000Z ] [ 1970-02-09T04:00:00.000Z, 1970-02-09T04:00:01.000Z ] [ 1970-02-10T04:00:00.000Z, 1970-02-10T04:00:01.000Z ] ... */ ``` # [Examples](https://effect.website/docs/scheduling/examples/) ## Overview These examples demonstrate different approaches to handling timeouts, retries, and periodic execution using Effect. Each scenario ensures that the application remains responsive and resilient to failures while adapting dynamically to various conditions. ## Handling Timeouts and Retries for API Calls When calling third-party APIs, it is often necessary to enforce timeouts and implement retry mechanisms to handle transient failures. In this example, the API call retries up to two times in case of failure and will be interrupted if it takes longer than 4 seconds. **Example** (Retrying an API Call with a Timeout) ```ts twoslash import { Console, Effect } from "effect" // Function to make the API call const getJson = (url: string) => Effect.tryPromise(() => fetch(url).then((res) => { if (!res.ok) { console.log("error") throw new Error(res.statusText) } console.log("ok") return res.json() as unknown }) ) // Program that retries the API call twice, times out after 4 seconds, // and logs errors const program = (url: string) => getJson(url).pipe( Effect.retry({ times: 2 }), Effect.timeout("4 seconds"), Effect.catchAll(Console.error) ) // Test case: successful API response Effect.runFork(program("https://dummyjson.com/products/1?delay=1000")) /* Output: ok */ // Test case: API call exceeding timeout limit Effect.runFork(program("https://dummyjson.com/products/1?delay=5000")) /* Output: TimeoutException: Operation timed out before the specified duration of '4s' elapsed */ // Test case: API returning an error response Effect.runFork(program("https://dummyjson.com/auth/products/1?delay=500")) /* Output: error error error UnknownException: An unknown error occurred */ ``` ## Retrying API Calls Based on Specific Errors Sometimes, retries should only happen for certain error conditions. For example, if an API call fails with a `401 Unauthorized` response, retrying might make sense, while a `404 Not Found` error should not trigger a retry. **Example** (Retrying Only on Specific Error Codes) ```ts twoslash import { Console, Effect } from "effect" // Custom error class for handling status codes class Err extends Error { constructor(message: string, readonly status: number) { super(message) } } // Function to make the API call const getJson = (url: string) => Effect.tryPromise({ try: () => fetch(url).then((res) => { if (!res.ok) { console.log(res.status) throw new Err(res.statusText, res.status) } return res.json() as unknown }), catch: (e) => e as Err }) // Program that retries only when the error status is 401 (Unauthorized) const program = (url: string) => getJson(url).pipe( Effect.retry({ while: (err) => err.status === 401 }), Effect.catchAll(Console.error) ) // Test case: API returns 401 (triggers multiple retries) Effect.runFork( program("https://dummyjson.com/auth/products/1?delay=1000") ) /* Output: 401 401 401 401 ... */ // Test case: API returns 404 (no retries) Effect.runFork(program("https://dummyjson.com/-")) /* Output: 404 Err [Error]: Not Found */ ``` ## Retrying with Dynamic Delays Based on Error Information Some API errors, such as `429 Too Many Requests`, include a `Retry-After` header that specifies how long to wait before retrying. Instead of using a fixed delay, we can dynamically adjust the retry interval based on this value. **Example** (Using the `Retry-After` Header for Retry Delays) This approach ensures that the retry delay adapts dynamically to the server's response, preventing unnecessary retries while respecting the provided `Retry-After` value. ```ts twoslash import { Duration, Effect, Schedule } from "effect" // Custom error class representing a "Too Many Requests" response class TooManyRequestsError { readonly _tag = "TooManyRequestsError" constructor(readonly retryAfter: number) {} } let n = 1 const request = Effect.gen(function* () { // Simulate failing a particular number of times if (n < 3) { const retryAfter = n * 500 console.log(`Attempt #${n++}, retry after ${retryAfter} millis...`) // Simulate retrieving the retry-after header return yield* Effect.fail(new TooManyRequestsError(retryAfter)) } console.log("Done") return "some result" }) // Retry policy that extracts the retry delay from the error const policy = Schedule.identity().pipe( Schedule.addDelay((error) => error._tag === "TooManyRequestsError" ? // Wait for the specified retry-after duration Duration.millis(error.retryAfter) : Duration.zero ), // Limit retries to 5 attempts Schedule.intersect(Schedule.recurs(5)) ) const program = request.pipe(Effect.retry(policy)) Effect.runFork(program) /* Output: Attempt #1, retry after 500 millis... Attempt #2, retry after 1000 millis... Done */ ``` ## Running Periodic Tasks Until Another Task Completes There are cases where we need to repeatedly perform an action at fixed intervals until another longer-running task finishes. This pattern is common in polling mechanisms or periodic logging. **Example** (Running a Scheduled Task Until Completion) ```ts twoslash import { Effect, Console, Schedule } from "effect" // Define a long-running effect // (e.g., a task that takes 5 seconds to complete) const longRunningEffect = Console.log("done").pipe( Effect.delay("5 seconds") ) // Define an action to run periodically const action = Console.log("action...") // Define a fixed interval schedule const schedule = Schedule.fixed("1.5 seconds") // Run the action repeatedly until the long-running task completes const program = Effect.race( Effect.repeat(action, schedule), longRunningEffect ) Effect.runPromise(program) /* Output: action... action... action... action... done */ ``` # [Introduction](https://effect.website/docs/scheduling/introduction/) ## Overview # Scheduling Scheduling is an important concept in Effect that allows you to define recurring effectful operations. It involves the use of the `Schedule` type, which is an immutable value that describes a scheduled pattern for executing effects. The `Schedule` type is structured as follows: ```text showLineNumbers=false ┌─── The type of output produced by the schedule │ ┌─── The type of input consumed by the schedule │ │ ┌─── Additional requirements for the schedule ▼ ▼ ▼ Schedule ``` A schedule operates by consuming values of type `In` (such as errors in the case of `retry`, or values in the case of `repeat`) and producing values of type `Out`. It determines when to halt or continue the execution based on input values and its internal state. The inclusion of a `Requirements` parameter allows the schedule to leverage additional services or resources as needed. Schedules are defined as a collection of intervals spread out over time. Each interval represents a window during which the recurrence of an effect is possible. ## Retrying and Repetition In the realm of scheduling, there are two related concepts: [Retrying](/docs/error-management/retrying/) and [Repetition](/docs/scheduling/repetition/). While they share the same underlying idea, they differ in their focus. Retrying aims to handle failures by executing an effect again, while repetition focuses on executing an effect repeatedly to achieve a desired outcome. When using schedules for retrying or repetition, each interval's starting boundary determines when the effect will be executed again. For example, in retrying, if an error occurs, the schedule defines when the effect should be retried. ## Composability of Schedules Schedules are composable, meaning you can combine simple schedules to create more complex recurrence patterns. Operators like `Schedule.union` or `Schedule.intersect` allow you to build sophisticated schedules by combining and modifying existing ones. This flexibility enables you to tailor the scheduling behavior to meet specific requirements. # [Repetition](https://effect.website/docs/scheduling/repetition/) ## Overview import { Aside } from "@astrojs/starlight/components" Repetition is a common requirement when working with effects in software development. It allows us to perform an effect multiple times according to a specific repetition policy. ## repeat The `Effect.repeat` function returns a new effect that repeats the given effect according to a specified schedule or until the first failure. **Example** (Repeating a Successful Effect) ```ts twoslash import { Effect, Schedule, Console } from "effect" // Define an effect that logs a message to the console const action = Console.log("success") // Define a schedule that repeats the action 2 more times with a delay const policy = Schedule.addDelay(Schedule.recurs(2), () => "100 millis") // Repeat the action according to the schedule const program = Effect.repeat(action, policy) // Run the program and log the number of repetitions Effect.runPromise(program).then((n) => console.log(`repetitions: ${n}`)) /* Output: success success success repetitions: 2 */ ``` **Example** (Handling Failures in Repetition) ```ts twoslash import { Effect, Schedule } from "effect" let count = 0 // Define an async effect that simulates an action with potential failure const action = Effect.async((resume) => { if (count > 1) { console.log("failure") resume(Effect.fail("Uh oh!")) } else { count++ console.log("success") resume(Effect.succeed("yay!")) } }) // Define a schedule that repeats the action 2 more times with a delay const policy = Schedule.addDelay(Schedule.recurs(2), () => "100 millis") // Repeat the action according to the schedule const program = Effect.repeat(action, policy) // Run the program and observe the result on failure Effect.runPromiseExit(program).then(console.log) /* Output: success success failure { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Uh oh!' } } */ ``` ### Skipping First Execution If you want to avoid the first execution and only run the action according to a schedule, you can use `Effect.schedule`. This allows the effect to skip the initial run and follow the defined repeat policy. **Example** (Skipping First Execution) ```ts twoslash import { Effect, Schedule, Console } from "effect" const action = Console.log("success") const policy = Schedule.addDelay(Schedule.recurs(2), () => "100 millis") const program = Effect.schedule(action, policy) Effect.runPromise(program).then((n) => console.log(`repetitions: ${n}`)) /* Output: success success repetitions: 2 */ ``` ## repeatN The `repeatN` function returns a new effect that repeats the specified effect a given number of times or until the first failure. The repeats are in addition to the initial execution, so `Effect.repeatN(action, 1)` executes `action` once initially and then repeats it one additional time if it succeeds. **Example** (Repeating an Action Multiple Times) ```ts twoslash import { Effect, Console } from "effect" const action = Console.log("success") // Repeat the action 2 additional times after the first execution const program = Effect.repeatN(action, 2) Effect.runPromise(program) /* Output: success success success */ ``` ## repeatOrElse The `repeatOrElse` function returns a new effect that repeats the specified effect according to the given schedule or until the first failure. When a failure occurs, the failure value and schedule output are passed to a specified handler. Scheduled recurrences are in addition to the initial execution, so `Effect.repeat(action, Schedule.once)` executes `action` once initially and then repeats it an additional time if it succeeds. **Example** (Handling Failure During Repeats) ```ts twoslash import { Effect, Schedule } from "effect" let count = 0 // Define an async effect that simulates an action with possible failures const action = Effect.async((resume) => { if (count > 1) { console.log("failure") resume(Effect.fail("Uh oh!")) } else { count++ console.log("success") resume(Effect.succeed("yay!")) } }) // Define a schedule that repeats up to 2 times // with a 100ms delay between attempts const policy = Schedule.addDelay(Schedule.recurs(2), () => "100 millis") // Provide a handler to run when failure occurs after the retries const program = Effect.repeatOrElse(action, policy, () => Effect.sync(() => { console.log("orElse") return count - 1 }) ) Effect.runPromise(program).then((n) => console.log(`repetitions: ${n}`)) /* Output: success success failure orElse repetitions: 1 */ ``` ## Repeating Based on a Condition You can control the repetition of an effect by a condition using either a `while` or `until` option, allowing for dynamic control based on runtime outcomes. **Example** (Repeating Until a Condition is Met) ```ts twoslash import { Effect } from "effect" let count = 0 // Define an effect that simulates varying outcomes on each invocation const action = Effect.sync(() => { console.log(`Action called ${++count} time(s)`) return count }) // Repeat the action until the count reaches 3 const program = Effect.repeat(action, { until: (n) => n === 3 }) Effect.runFork(program) /* Output: Action called 1 time(s) Action called 2 time(s) Action called 3 time(s) */ ``` # [Schedule Combinators](https://effect.website/docs/scheduling/schedule-combinators/) ## Overview import { Aside } from "@astrojs/starlight/components" Schedules define stateful, possibly effectful, recurring schedules of events, and compose in a variety of ways. Combinators allow us to take schedules and combine them together to get other schedules. To demonstrate the functionality of different schedules, we will use the following helper function that logs each repetition along with the corresponding delay in milliseconds, formatted as: ```text showLineNumbers=false #: ``` **Helper** (Logging Execution Delays) ```ts twoslash import { Array, Chunk, Duration, Effect, Schedule } from "effect" const log = ( schedule: Schedule.Schedule, delay: Duration.DurationInput = 0 ): void => { const maxRecurs = 10 // Limit the number of executions const delays = Chunk.toArray( Effect.runSync( Schedule.run( Schedule.delays(Schedule.addDelay(schedule, () => delay)), Date.now(), Array.range(0, maxRecurs) ) ) ) delays.forEach((duration, i) => { console.log( i === maxRecurs ? "..." // Indicate truncation if there are more executions : i === delays.length - 1 ? "(end)" // Mark the last execution : `#${i + 1}: ${Duration.toMillis(duration)}ms` ) }) } ``` ## Composition Schedules can be composed in different ways: | Mode | Description | | ---------------- | -------------------------------------------------------------------------------------------------- | | **Union** | Combines two schedules and recurs if either schedule wants to continue, using the shorter delay. | | **Intersection** | Combines two schedules and recurs only if both schedules want to continue, using the longer delay. | | **Sequencing** | Combines two schedules by running the first one fully, then switching to the second. | ### Union Combines two schedules and recurs if either schedule wants to continue, using the shorter delay. **Example** (Combining Exponential and Spaced Intervals) ```ts twoslash collapse={3-26} import { Array, Chunk, Duration, Effect, Schedule } from "effect" const log = ( schedule: Schedule.Schedule, delay: Duration.DurationInput = 0 ): void => { const maxRecurs = 10 const delays = Chunk.toArray( Effect.runSync( Schedule.run( Schedule.delays(Schedule.addDelay(schedule, () => delay)), Date.now(), Array.range(0, maxRecurs) ) ) ) delays.forEach((duration, i) => { console.log( i === maxRecurs ? "..." : i === delays.length - 1 ? "(end)" : `#${i + 1}: ${Duration.toMillis(duration)}ms` ) }) } const schedule = Schedule.union( Schedule.exponential("100 millis"), Schedule.spaced("1 second") ) log(schedule) /* Output: #1: 100ms < exponential #2: 200ms #3: 400ms #4: 800ms #5: 1000ms < spaced #6: 1000ms #7: 1000ms #8: 1000ms #9: 1000ms #10: 1000ms ... */ ``` The `Schedule.union` operator selects the shortest delay at each step, so when combining an exponential schedule with a spaced interval, the initial recurrences will follow the exponential backoff, then settle into the spaced interval once the delays exceed that value. ### Intersection Combines two schedules and recurs only if both schedules want to continue, using the longer delay. **Example** (Limiting Exponential Backoff with a Fixed Number of Retries) ```ts twoslash collapse={3-26} import { Array, Chunk, Duration, Effect, Schedule } from "effect" const log = ( schedule: Schedule.Schedule, delay: Duration.DurationInput = 0 ): void => { const maxRecurs = 10 const delays = Chunk.toArray( Effect.runSync( Schedule.run( Schedule.delays(Schedule.addDelay(schedule, () => delay)), Date.now(), Array.range(0, maxRecurs) ) ) ) delays.forEach((duration, i) => { console.log( i === maxRecurs ? "..." : i === delays.length - 1 ? "(end)" : `#${i + 1}: ${Duration.toMillis(duration)}ms` ) }) } const schedule = Schedule.intersect( Schedule.exponential("10 millis"), Schedule.recurs(5) ) log(schedule) /* Output: #1: 10ms < exponential #2: 20ms #3: 40ms #4: 80ms #5: 160ms (end) < recurs */ ``` The `Schedule.intersect` operator enforces both schedules' constraints. In this example, the schedule follows an exponential backoff but stops after 5 recurrences due to the `Schedule.recurs(5)` limit. ### Sequencing Combines two schedules by running the first one fully, then switching to the second. **Example** (Switching from Fixed Retries to Periodic Execution) ```ts twoslash collapse={3-26} import { Array, Chunk, Duration, Effect, Schedule } from "effect" const log = ( schedule: Schedule.Schedule, delay: Duration.DurationInput = 0 ): void => { const maxRecurs = 10 const delays = Chunk.toArray( Effect.runSync( Schedule.run( Schedule.delays(Schedule.addDelay(schedule, () => delay)), Date.now(), Array.range(0, maxRecurs) ) ) ) delays.forEach((duration, i) => { console.log( i === maxRecurs ? "..." : i === delays.length - 1 ? "(end)" : `#${i + 1}: ${Duration.toMillis(duration)}ms` ) }) } const schedule = Schedule.andThen( Schedule.recurs(5), Schedule.spaced("1 second") ) log(schedule) /* Output: #1: 0ms < recurs #2: 0ms #3: 0ms #4: 0ms #5: 0ms #6: 1000ms < spaced #7: 1000ms #8: 1000ms #9: 1000ms #10: 1000ms ... */ ``` The first schedule runs until completion, after which the second schedule takes over. In this example, the effect initially executes 5 times with no delay, then continues every 1 second. ## Adding Randomness to Retry Delays The `Schedule.jittered` combinator modifies a schedule by applying a random delay within a specified range. When a resource is out of service due to overload or contention, retrying and backing off doesn't help us. If all failed API calls are backed off to the same point of time, they cause another overload or contention. Jitter adds some amount of randomness to the delay of the schedule. This helps us to avoid ending up accidentally synchronizing and taking the service down by accident. [Research](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) suggests that `Schedule.jittered(0.0, 1.0)` is an effective way to introduce randomness in retries. **Example** (Jittered Exponential Backoff) ```ts twoslash collapse={3-26} import { Array, Chunk, Duration, Effect, Schedule } from "effect" const log = ( schedule: Schedule.Schedule, delay: Duration.DurationInput = 0 ): void => { const maxRecurs = 10 const delays = Chunk.toArray( Effect.runSync( Schedule.run( Schedule.delays(Schedule.addDelay(schedule, () => delay)), Date.now(), Array.range(0, maxRecurs) ) ) ) delays.forEach((duration, i) => { console.log( i === maxRecurs ? "..." : i === delays.length - 1 ? "(end)" : `#${i + 1}: ${Duration.toMillis(duration)}ms` ) }) } const schedule = Schedule.jittered(Schedule.exponential("10 millis")) log(schedule) /* Output: #1: 10.448486ms #2: 21.134521ms #3: 47.245117ms #4: 88.263184ms #5: 163.651367ms #6: 335.818848ms #7: 719.126709ms #8: 1266.18457ms #9: 2931.252441ms #10: 6121.593018ms ... */ ``` The `Schedule.jittered` combinator introduces randomness to delays within a range. For example, applying jitter to an exponential backoff ensures that each retry occurs at a slightly different time, reducing the risk of overwhelming the system. ## Controlling Repetitions with Filters You can use `Schedule.whileInput` or `Schedule.whileOutput` to limit how long a schedule continues based on conditions applied to its input or output. **Example** (Stopping Based on Output) ```ts twoslash collapse={3-26} import { Array, Chunk, Duration, Effect, Schedule } from "effect" const log = ( schedule: Schedule.Schedule, delay: Duration.DurationInput = 0 ): void => { const maxRecurs = 10 const delays = Chunk.toArray( Effect.runSync( Schedule.run( Schedule.delays(Schedule.addDelay(schedule, () => delay)), Date.now(), Array.range(0, maxRecurs) ) ) ) delays.forEach((duration, i) => { console.log( i === maxRecurs ? "..." : i === delays.length - 1 ? "(end)" : `#${i + 1}: ${Duration.toMillis(duration)}ms` ) }) } const schedule = Schedule.whileOutput(Schedule.recurs(5), (n) => n <= 2) log(schedule) /* Output: #1: 0ms < recurs #2: 0ms #3: 0ms (end) < whileOutput */ ``` `Schedule.whileOutput` filters repetitions based on the output of the schedule. In this example, the schedule stops once the output exceeds `2`, even though `Schedule.recurs(5)` allows up to 5 repetitions. ## Adjusting Delays Based on Output The `Schedule.modifyDelay` combinator allows you to dynamically change the delay of a schedule based on the number of repetitions or other output conditions. **Example** (Reducing Delay After a Certain Number of Repetitions) ```ts twoslash collapse={3-26} import { Array, Chunk, Duration, Effect, Schedule } from "effect" const log = ( schedule: Schedule.Schedule, delay: Duration.DurationInput = 0 ): void => { const maxRecurs = 10 const delays = Chunk.toArray( Effect.runSync( Schedule.run( Schedule.delays(Schedule.addDelay(schedule, () => delay)), Date.now(), Array.range(0, maxRecurs) ) ) ) delays.forEach((duration, i) => { console.log( i === maxRecurs ? "..." : i === delays.length - 1 ? "(end)" : `#${i + 1}: ${Duration.toMillis(duration)}ms` ) }) } const schedule = Schedule.modifyDelay( Schedule.spaced("1 second"), (out, duration) => (out > 2 ? "100 millis" : duration) ) log(schedule) /* Output: #1: 1000ms #2: 1000ms #3: 1000ms #4: 100ms < modifyDelay #5: 100ms #6: 100ms #7: 100ms #8: 100ms #9: 100ms #10: 100ms ... */ ``` The delay modification applies dynamically during execution. In this example, the first three repetitions follow the original `1-second` spacing. After that, the delay drops to `100 milliseconds`, making subsequent repetitions occur more frequently. ## Tapping `Schedule.tapInput` and `Schedule.tapOutput` allow you to perform additional effectful operations on a schedule's input or output without modifying its behavior. **Example** (Logging Schedule Outputs) ```ts twoslash collapse={3-26} import { Array, Chunk, Duration, Effect, Schedule, Console } from "effect" const log = ( schedule: Schedule.Schedule, delay: Duration.DurationInput = 0 ): void => { const maxRecurs = 10 const delays = Chunk.toArray( Effect.runSync( Schedule.run( Schedule.delays(Schedule.addDelay(schedule, () => delay)), Date.now(), Array.range(0, maxRecurs) ) ) ) delays.forEach((duration, i) => { console.log( i === maxRecurs ? "..." : i === delays.length - 1 ? "(end)" : `#${i + 1}: ${Duration.toMillis(duration)}ms` ) }) } const schedule = Schedule.tapOutput(Schedule.recurs(2), (n) => Console.log(`Schedule Output: ${n}`) ) log(schedule) /* Output: Schedule Output: 0 Schedule Output: 1 Schedule Output: 2 #1: 0ms #2: 0ms (end) */ ``` `Schedule.tapOutput` runs an effect before each recurrence, using the schedule's current output as input. This can be useful for logging, debugging, or triggering side effects. # [Advanced Usage](https://effect.website/docs/schema/advanced-usage/) ## Overview import { Aside } from "@astrojs/starlight/components" ## Declaring New Data Types ### Primitive Data Types To declare a schema for a primitive data type, such as `File`, you can use the `Schema.declare` function along with a type guard. **Example** (Declaring a Schema for `File`) ```ts twoslash import { Schema } from "effect" // Declare a schema for the File type using a type guard const FileFromSelf = Schema.declare( (input: unknown): input is File => input instanceof File ) const decode = Schema.decodeUnknownSync(FileFromSelf) // Decoding a valid File object console.log(decode(new File([], ""))) /* Output: File { size: 0, type: '', name: '', lastModified: 1724774163056 } */ // Decoding an invalid input decode(null) /* throws ParseError: Expected , actual null */ ``` To enhance the default error message, you can add annotations, particularly the `identifier`, `title`, and `description` annotations (none of these annotations are required, but they are encouraged for good practice and can make your schema "self-documenting"). These annotations will be utilized by the messaging system to return more meaningful messages. - **Identifier**: a unique name for the schema - **Title**: a brief, descriptive title - **Description**: a detailed explanation of the schema's purpose **Example** (Declaring a Schema with Annotations) ```ts twoslash import { Schema } from "effect" // Declare a schema for the File type with additional annotations const FileFromSelf = Schema.declare( (input: unknown): input is File => input instanceof File, { // A unique identifier for the schema identifier: "FileFromSelf", // Detailed description of the schema description: "The `File` type in JavaScript" } ) const decode = Schema.decodeUnknownSync(FileFromSelf) // Decoding a valid File object console.log(decode(new File([], ""))) /* Output: File { size: 0, type: '', name: '', lastModified: 1724774163056 } */ // Decoding an invalid input decode(null) /* throws ParseError: Expected FileFromSelf, actual null */ ``` ### Type Constructors Type constructors are generic types that take one or more types as arguments and return a new type. To define a schema for a type constructor, you can use the `Schema.declare` function. **Example** (Declaring a Schema for `ReadonlySet`) ```ts twoslash import { ParseResult, Schema } from "effect" export const MyReadonlySet = ( // Schema for the elements of the Set item: Schema.Schema ): Schema.Schema, ReadonlySet, R> => Schema.declare( // Store the schema for the Set's elements [item], { // Decoding function decode: (item) => (input, parseOptions, ast) => { if (input instanceof Set) { // Decode each element in the Set const elements = ParseResult.decodeUnknown(Schema.Array(item))( Array.from(input.values()), parseOptions ) // Return a ReadonlySet containing the decoded elements return ParseResult.map( elements, (as): ReadonlySet => new Set(as) ) } // Handle invalid input return ParseResult.fail(new ParseResult.Type(ast, input)) }, // Encoding function encode: (item) => (input, parseOptions, ast) => { if (input instanceof Set) { // Encode each element in the Set const elements = ParseResult.encodeUnknown(Schema.Array(item))( Array.from(input.values()), parseOptions ) // Return a ReadonlySet containing the encoded elements return ParseResult.map( elements, (is): ReadonlySet => new Set(is) ) } // Handle invalid input return ParseResult.fail(new ParseResult.Type(ast, input)) } }, { description: `ReadonlySet<${Schema.format(item)}>` } ) // Define a schema for a ReadonlySet of numbers const setOfNumbers = MyReadonlySet(Schema.NumberFromString) const decode = Schema.decodeUnknownSync(setOfNumbers) console.log(decode(new Set(["1", "2", "3"]))) // Set(3) { 1, 2, 3 } // Decode an invalid input decode(null) /* throws ParseError: Expected ReadonlySet, actual null */ // Decode a Set with an invalid element decode(new Set(["1", null, "3"])) /* throws ParseError: ReadonlyArray └─ [1] └─ NumberFromString └─ Encoded side transformation failure └─ Expected string, actual null */ ``` ### Adding Compilers Annotations When defining a new data type, some compilers like [Arbitrary](/docs/schema/arbitrary/) or [Pretty](/docs/schema/pretty/) may not know how to handle the new type. This can result in an error, as the compiler may lack the necessary information for generating instances or producing readable output: **Example** (Attempting to Generate Arbitrary Values Without Required Annotations) ```ts twoslash import { Arbitrary, Schema } from "effect" // Define a schema for the File type const FileFromSelf = Schema.declare( (input: unknown): input is File => input instanceof File, { identifier: "FileFromSelf" } ) // Try creating an Arbitrary instance for the schema const arb = Arbitrary.make(FileFromSelf) /* throws: Error: Missing annotation details: Generating an Arbitrary for this schema requires an "arbitrary" annotation schema (Declaration): FileFromSelf */ ``` In the above example, attempting to generate arbitrary values for the `FileFromSelf` schema fails because the compiler lacks necessary annotations. To resolve this, you need to provide annotations for generating arbitrary data: **Example** (Adding Arbitrary Annotation for Custom `File` Schema) ```ts twoslash import { Arbitrary, FastCheck, Pretty, Schema } from "effect" const FileFromSelf = Schema.declare( (input: unknown): input is File => input instanceof File, { identifier: "FileFromSelf", // Provide a function to generate random File instances arbitrary: () => (fc) => fc .tuple(fc.string(), fc.string()) .map(([content, path]) => new File([content], path)) } ) // Create an Arbitrary instance for the schema const arb = Arbitrary.make(FileFromSelf) // Generate sample files using the Arbitrary instance const files = FastCheck.sample(arb, 2) console.log(files) /* Example Output: [ File { size: 5, type: '', name: 'C', lastModified: 1706435571176 }, File { size: 1, type: '', name: '98Ggmc', lastModified: 1706435571176 } ] */ ``` For more details on how to add annotations for the Arbitrary compiler, refer to the [Arbitrary](/docs/schema/arbitrary/) documentation. ## Branded types TypeScript's type system is structural, which means that any two types that are structurally equivalent are considered the same. This can cause issues when types that are semantically different are treated as if they were the same. **Example** (Structural Typing Issue) ```ts twoslash type UserId = string type Username = string declare const getUser: (id: UserId) => object const myUsername: Username = "gcanti" getUser(myUsername) // This erroneously works ``` In the above example, `UserId` and `Username` are both aliases for the same type, `string`. This means that the `getUser` function can mistakenly accept a `Username` as a valid `UserId`, causing bugs and errors. To prevent this, Effect introduces **branded types**. These types attach a unique identifier (or "brand") to a type, allowing you to differentiate between structurally similar but semantically distinct types. **Example** (Defining Branded Types) ```ts twoslash import { Brand } from "effect" type UserId = string & Brand.Brand<"UserId"> type Username = string declare const getUser: (id: UserId) => object const myUsername: Username = "gcanti" // @ts-expect-error getUser(myUsername) /* Argument of type 'string' is not assignable to parameter of type 'UserId'. Type 'string' is not assignable to type 'Brand<"UserId">'.ts(2345) */ ``` By defining `UserId` as a branded type, the `getUser` function can accept only values of type `UserId`, and not plain strings or other types that are compatible with strings. This helps to prevent bugs caused by accidentally passing the wrong type of value to the function. There are two ways to define a schema for a branded type, depending on whether you: - want to define the schema from scratch - have already defined a branded type via [`effect/Brand`](/docs/code-style/branded-types/) and want to reuse it to define a schema ### Defining a brand schema from scratch To define a schema for a branded type from scratch, use the `Schema.brand` function. **Example** (Creating a schema for a Branded Type) ```ts twoslash import { Schema } from "effect" const UserId = Schema.String.pipe(Schema.brand("UserId")) // string & Brand<"UserId"> type UserId = typeof UserId.Type ``` Note that you can use `unique symbol`s as brands to ensure uniqueness across modules / packages. **Example** (Using a unique symbol as a Brand) ```ts twoslash import { Schema } from "effect" const UserIdBrand: unique symbol = Symbol.for("UserId") const UserId = Schema.String.pipe(Schema.brand(UserIdBrand)) // string & Brand type UserId = typeof UserId.Type ``` ### Reusing an existing branded constructor If you have already defined a branded type using the [`effect/Brand`](/docs/code-style/branded-types/) module, you can reuse it to define a schema using the `Schema.fromBrand` function. **Example** (Reusing an Existing Branded Type) ```ts twoslash import { Schema } from "effect" import { Brand } from "effect" // the existing branded type type UserId = string & Brand.Brand<"UserId"> const UserId = Brand.nominal() // Define a schema for the branded type const UserIdSchema = Schema.String.pipe(Schema.fromBrand(UserId)) ``` ### Utilizing Default Constructors The `Schema.brand` function includes a default constructor to facilitate the creation of branded values. ```ts twoslash import { Schema } from "effect" const UserId = Schema.String.pipe(Schema.brand("UserId")) const userId = UserId.make("123") // Creates a branded UserId ``` ## Property Signatures A `PropertySignature` represents a transformation from a "From" field to a "To" field. This allows you to define mappings between incoming data fields and your internal model. ### Basic Usage A property signature can be defined with annotations to provide additional context about a field. **Example** (Adding Annotations to a Property Signature) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.propertySignature(Schema.NumberFromString).annotations({ title: "Age" // Annotation to label the age field }) }) ``` A `PropertySignature` type contains several parameters, each providing details about the transformation between the source field (From) and the target field (To). Let's take a look at what each of these parameters represents: ```ts showLineNumbers=false age: PropertySignature< ToToken, ToType, FromKey, FromToken, FromType, HasDefault, Context > ``` | Parameter | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------- | | `age` | Key of the "To" field | | `ToToken` | Indicates field requirement: `"?:"` for optional, `":"` for required | | `ToType` | Type of the "To" field | | `FromKey` | (Optional, default = `never`) Indicates the source field key, typically the same as "To" field key unless specified | | `FromToken` | Indicates source field requirement: `"?:"` for optional, `":"` for required | | `FromType` | Type of the "From" field | | `HasDefault` | Indicates if there is a constructor default value (Boolean) | In the example above, the `PropertySignature` type for `age` is: ```ts showLineNumbers=false PropertySignature<":", number, never, ":", string, false, never> ``` This means: | Parameter | Description | | ------------ | -------------------------------------------------------------------------- | | `age` | Key of the "To" field | | `ToToken` | `":"` indicates that the `age` field is required | | `ToType` | Type of the `age` field is `number` | | `FromKey` | `never` indicates that the decoding occurs from the same field named `age` | | `FromToken` | `":"` indicates that the decoding occurs from a required `age` field | | `FromType` | Type of the "From" field is `string` | | `HasDefault` | `false`: indicates there is no default value | Sometimes, the source field (the "From" field) may have a different name from the field in your internal model. You can map between these fields using the `Schema.fromKey` function. **Example** (Mapping from a Different Key) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.propertySignature(Schema.NumberFromString).pipe( Schema.fromKey("AGE") // Maps from "AGE" to "age" ) }) console.log(Schema.decodeUnknownSync(Person)({ name: "name", AGE: "18" })) // Output: { name: 'name', age: 18 } ``` When you map from `"AGE"` to `"age"`, the `PropertySignature` type changes to: ```ts showLineNumbers=false ""AGE"" del={1} ins={2} PropertySignature<":", number, never, ":", string, false, never> PropertySignature<":", number, "AGE", ":", string, false, never> ``` ### Optional Fields #### Basic Optional Property The syntax: ```ts showLineNumbers=false Schema.optional(schema: Schema) ``` creates an optional property within a schema, allowing fields to be omitted or set to `undefined`. ##### Decoding | Input | Output | | ----------------- | ------------------------- | | `` | remains `` | | `undefined` | remains `undefined` | | `i: I` | transforms to `a: A` | ##### Encoding | Input | Output | | ----------------- | ------------------------- | | `` | remains `` | | `undefined` | remains `undefined` | | `a: A` | transforms back to `i: I` | **Example** (Defining an Optional Number Field) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optional(Schema.NumberFromString) }) // ┌─── { readonly quantity?: string | undefined; } // ▼ type Encoded = typeof Product.Encoded // ┌─── { readonly quantity?: number | undefined; } // ▼ type Type = typeof Product.Type // Decoding examples console.log(Schema.decodeUnknownSync(Product)({ quantity: "1" })) // Output: { quantity: 1 } console.log(Schema.decodeUnknownSync(Product)({})) // Output: {} console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined })) // Output: { quantity: undefined } // Encoding examples console.log(Schema.encodeSync(Product)({ quantity: 1 })) // Output: { quantity: "1" } console.log(Schema.encodeSync(Product)({})) // Output: {} console.log(Schema.encodeSync(Product)({ quantity: undefined })) // Output: { quantity: undefined } ``` ##### Exposed Values You can access the original schema type (before it was marked as optional) using the `from` property. **Example** (Accessing the Original Schema) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optional(Schema.NumberFromString) }) // ┌─── typeof Schema.NumberFromString // ▼ const from = Product.fields.quantity.from ``` #### Optional with Nullability The syntax: ```ts showLineNumbers=false Schema.optionalWith(schema: Schema, { nullable: true }) ``` creates an optional property within a schema, treating `null` values the same as missing values. ##### Decoding | Input | Output | | ----------------- | ------------------------------- | | `` | remains `` | | `undefined` | remains `undefined` | | `null` | transforms to `` | | `i: I` | transforms to `a: A` | ##### Encoding | Input | Output | | ----------------- | ------------------------- | | `` | remains `` | | `undefined` | remains `undefined` | | `a: A` | transforms back to `i: I` | **Example** (Handling Null as Missing Value) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { nullable: true }) }) // ┌─── { readonly quantity?: string | null | undefined; } // ▼ type Encoded = typeof Product.Encoded // ┌─── { readonly quantity?: number | undefined; } // ▼ type Type = typeof Product.Type // Decoding examples console.log(Schema.decodeUnknownSync(Product)({ quantity: "1" })) // Output: { quantity: 1 } console.log(Schema.decodeUnknownSync(Product)({})) // Output: {} console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined })) // Output: { quantity: undefined } console.log(Schema.decodeUnknownSync(Product)({ quantity: null })) // Output: {} // Encoding examples console.log(Schema.encodeSync(Product)({ quantity: 1 })) // Output: { quantity: "1" } console.log(Schema.encodeSync(Product)({})) // Output: {} console.log(Schema.encodeSync(Product)({ quantity: undefined })) // Output: { quantity: undefined } ``` ##### Exposed Values You can access the original schema type (before it was marked as optional) using the `from` property. **Example** (Accessing the Original Schema) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { nullable: true }) }) // ┌─── typeof Schema.NumberFromString // ▼ const from = Product.fields.quantity.from ``` #### Optional with Exactness The syntax: ```ts showLineNumbers=false Schema.optionalWith(schema: Schema, { exact: true }) ``` creates an optional property while enforcing strict typing. This means that only the specified type (excluding `undefined`) is accepted. Any attempt to decode `undefined` results in an error. ##### Decoding | Input | Output | | ----------------- | ------------------------- | | `` | remains `` | | `undefined` | `ParseError` | | `i: I` | transforms to `a: A` | ##### Encoding | Input | Output | | ----------------- | ------------------------- | | `` | remains `` | | `a: A` | transforms back to `i: I` | **Example** (Using Exactness with Optional Field) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { exact: true }) }) // ┌─── { readonly quantity?: string; } // ▼ type Encoded = typeof Product.Encoded // ┌─── { readonly quantity?: number; } // ▼ type Type = typeof Product.Type // Decoding examples console.log(Schema.decodeUnknownSync(Product)({ quantity: "1" })) // Output: { quantity: 1 } console.log(Schema.decodeUnknownSync(Product)({})) // Output: {} console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined })) /* throws: ParseError: { readonly quantity?: NumberFromString } └─ ["quantity"] └─ NumberFromString └─ Encoded side transformation failure └─ Expected string, actual undefined */ // Encoding examples console.log(Schema.encodeSync(Product)({ quantity: 1 })) // Output: { quantity: "1" } console.log(Schema.encodeSync(Product)({})) // Output: {} ``` ##### Exposed Values You can access the original schema type (before it was marked as optional) using the `from` property. **Example** (Accessing the Original Schema) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { exact: true }) }) // ┌─── typeof Schema.NumberFromString // ▼ const from = Product.fields.quantity.from ``` #### Combining Nullability and Exactness The syntax: ```ts showLineNumbers=false Schema.optionalWith(schema: Schema, { exact: true, nullable: true }) ``` allows you to define an optional property that enforces strict typing (exact type only) while also treating `null` as equivalent to a missing value. ##### Decoding | Input | Output | | ----------------- | ------------------------------- | | `` | remains `` | | `null` | transforms to `` | | `undefined` | `ParseError` | | `i: I` | transforms to `a: A` | ##### Encoding | Input | Output | | ----------------- | ------------------------- | | `` | remains `` | | `a: A` | transforms back to `i: I` | **Example** (Using Exactness and Handling Null as Missing Value with Optional Field) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { exact: true, nullable: true }) }) // ┌─── { readonly quantity?: string | null; } // ▼ type Encoded = typeof Product.Encoded // ┌─── { readonly quantity?: number; } // ▼ type Type = typeof Product.Type // Decoding examples console.log(Schema.decodeUnknownSync(Product)({ quantity: "1" })) // Output: { quantity: 1 } console.log(Schema.decodeUnknownSync(Product)({})) // Output: {} console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined })) /* throws: ParseError: (Struct (Encoded side) <-> Struct (Type side)) └─ Encoded side transformation failure └─ Struct (Encoded side) └─ ["quantity"] └─ NumberFromString | null ├─ NumberFromString │ └─ Encoded side transformation failure │ └─ Expected string, actual undefined └─ Expected null, actual undefined */ console.log(Schema.decodeUnknownSync(Product)({ quantity: null })) // Output: {} // Encoding examples console.log(Schema.encodeSync(Product)({ quantity: 1 })) // Output: { quantity: "1" } console.log(Schema.encodeSync(Product)({})) // Output: {} ``` ##### Exposed Values You can access the original schema type (before it was marked as optional) using the `from` property. **Example** (Accessing the Original Schema) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { exact: true, nullable: true }) }) // ┌─── typeof Schema.NumberFromString // ▼ const from = Product.fields.quantity.from ``` ### Representing Optional Fields with never Type When creating a schema to replicate a TypeScript type that includes optional fields with the `never` type, like: ```ts type MyType = { readonly quantity?: never } ``` the handling of these fields depends on the `exactOptionalPropertyTypes` setting in your `tsconfig.json`. This setting affects whether the schema should treat optional `never`-typed fields as simply absent or allow `undefined` as a value. **Example** (`exactOptionalPropertyTypes: false`) When this feature is turned off, you can employ the `Schema.optional` function. This approach allows the field to implicitly accept `undefined` as a value. ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optional(Schema.Never) }) // ┌─── { readonly quantity?: undefined; } // ▼ type Encoded = typeof Product.Encoded // ┌─── { readonly quantity?: undefined; } // ▼ type Type = typeof Product.Type ``` **Example** (`exactOptionalPropertyTypes: true`) When this feature is turned on, the `Schema.optionalWith` function is recommended. It ensures stricter enforcement of the field's absence. ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.Never, { exact: true }) }) // ┌─── { readonly quantity?: never; } // ▼ type Encoded = typeof Product.Encoded // ┌─── { readonly quantity?: never; } // ▼ type Type = typeof Product.Type ``` ### Default Values The `default` option in `Schema.optionalWith` allows you to set default values that are applied during both decoding and object construction phases. This feature ensures that even if certain properties are not provided by the user, the system will automatically use the specified default values. The `Schema.optionalWith` function offers several ways to control how defaults are applied during decoding and encoding. You can fine-tune whether defaults are applied only when the input is completely missing, or even when `null` or `undefined` values are provided. #### Basic Default This is the simplest use case. If the input is missing or `undefined`, the default value will be applied. **Syntax** ```ts showLineNumbers=false Schema.optionalWith(schema: Schema, { default: () => A }) ``` | Operation | Behavior | | ------------ | ---------------------------------------------------------------- | | **Decoding** | Applies the default value if the input is missing or `undefined` | | **Encoding** | Transforms the input `a: A` back to `i: I` | **Example** (Applying Default When Field Is Missing or `undefined`) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { default: () => 1 // Default value for quantity }) }) // ┌─── { readonly quantity?: string | undefined; } // ▼ type Encoded = typeof Product.Encoded // ┌─── { readonly quantity: number; } // ▼ type Type = typeof Product.Type // Decoding examples with default applied console.log(Schema.decodeUnknownSync(Product)({})) // Output: { quantity: 1 } console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined })) // Output: { quantity: 1 } console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" })) // Output: { quantity: 2 } // Object construction examples with default applied console.log(Product.make({})) // Output: { quantity: 1 } console.log(Product.make({ quantity: 2 })) // Output: { quantity: 2 } ``` ##### Exposed Values You can access the original schema type (before it was marked as optional) using the `from` property. **Example** (Accessing the Original Schema) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { default: () => 1 // Default value for quantity }) }) // ┌─── typeof Schema.NumberFromString // ▼ const from = Product.fields.quantity.from ``` #### Default with Exactness When you want the default value to be applied only if the field is completely missing (not when it's `undefined`), you can use the `exact` option. **Syntax** ```ts showLineNumbers=false Schema.optionalWith(schema: Schema, { default: () => A, exact: true }) ``` | Operation | Behavior | | ------------ | ------------------------------------------------------ | | **Decoding** | Applies the default value only if the input is missing | | **Encoding** | Transforms the input `a: A` back to `i: I` | **Example** (Applying Default Only When Field Is Missing) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { default: () => 1, // Default value for quantity exact: true // Only apply default if quantity is not provided }) }) // ┌─── { readonly quantity?: string; } // ▼ type Encoded = typeof Product.Encoded // ┌─── { readonly quantity: number; } // ▼ type Type = typeof Product.Type console.log(Schema.decodeUnknownSync(Product)({})) // Output: { quantity: 1 } console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" })) // Output: { quantity: 2 } console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined })) /* throws: ParseError: (Struct (Encoded side) <-> Struct (Type side)) └─ Encoded side transformation failure └─ Struct (Encoded side) └─ ["quantity"] └─ NumberFromString └─ Encoded side transformation failure └─ Expected string, actual undefined */ ``` #### Default with Nullability In cases where you want `null` values to trigger the default behavior, you can use the `nullable` option. This ensures that if a field is set to `null`, it will be replaced by the default value. **Syntax** ```ts showLineNumbers=false Schema.optionalWith(schema: Schema, { default: () => A, nullable: true }) ``` | Operation | Behavior | | ------------ | -------------------------------------------------------------------------- | | **Decoding** | Applies the default value if the input is missing or `undefined` or `null` | | **Encoding** | Transforms the input `a: A` back to `i: I` | **Example** (Applying Default When Field Is Missing or `undefined` or `null`) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { default: () => 1, // Default value for quantity nullable: true // Apply default if quantity is null }) }) // ┌─── { readonly quantity?: string | null | undefined; } // ▼ type Encoded = typeof Product.Encoded // ┌─── { readonly quantity: number; } // ▼ type Type = typeof Product.Type console.log(Schema.decodeUnknownSync(Product)({})) // Output: { quantity: 1 } console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined })) // Output: { quantity: 1 } console.log(Schema.decodeUnknownSync(Product)({ quantity: null })) // Output: { quantity: 1 } console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" })) // Output: { quantity: 2 } ``` #### Combining Exactness and Nullability For a more strict approach, you can combine both `exact` and `nullable` options. This way, the default value is applied only when the field is `null` or missing, and not when it's explicitly set to `undefined`. **Syntax** ```ts showLineNumbers=false Schema.optionalWith(schema: Schema, { default: () => A, exact: true, nullable: true }) ``` | Operation | Behavior | | ------------ | ----------------------------------------------------------- | | **Decoding** | Applies the default value if the input is missing or `null` | | **Encoding** | Transforms the input `a: A` back to `i: I` | **Example** (Applying Default Only When Field Is Missing or `null`) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { default: () => 1, // Default value for quantity exact: true, // Only apply default if quantity is not provided nullable: true // Apply default if quantity is null }) }) // ┌─── { readonly quantity?: string | null; } // ▼ type Encoded = typeof Product.Encoded // ┌─── { readonly quantity: number; } // ▼ type Type = typeof Product.Type console.log(Schema.decodeUnknownSync(Product)({})) // Output: { quantity: 1 } console.log(Schema.decodeUnknownSync(Product)({ quantity: null })) // Output: { quantity: 1 } console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" })) // Output: { quantity: 2 } console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined })) /* throws: ParseError: (Struct (Encoded side) <-> Struct (Type side)) └─ Encoded side transformation failure └─ Struct (Encoded side) └─ ["quantity"] └─ NumberFromString └─ Encoded side transformation failure └─ Expected string, actual undefined */ ``` ### Optional Fields as Options When working with optional fields, you may want to handle them as [Option](/docs/data-types/option/) types. This approach allows you to explicitly manage the presence or absence of a field rather than relying on `undefined` or `null`. #### Basic Optional with Option Type You can configure a schema to treat optional fields as `Option` types, where missing or `undefined` values are converted to `Option.none()` and existing values are wrapped in `Option.some()`. **Syntax** ```ts showLineNumbers=false optionalWith(schema: Schema, { as: "Option" }) ``` ##### Decoding | Input | Output | | ----------------- | --------------------------------- | | `` | transforms to `Option.none()` | | `undefined` | transforms to `Option.none()` | | `i: I` | transforms to `Option.some(a: A)` | ##### Encoding | Input | Output | | ------------------- | ------------------------------- | | `Option.none()` | transforms to `` | | `Option.some(a: A)` | transforms back to `i: I` | **Example** (Handling Optional Field as Option) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { as: "Option" }) }) // ┌─── { readonly quantity?: string | undefined; } // ▼ type Encoded = typeof Product.Encoded // ┌─── { readonly quantity: Option; } // ▼ type Type = typeof Product.Type console.log(Schema.decodeUnknownSync(Product)({})) // Output: { quantity: { _id: 'Option', _tag: 'None' } } console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined })) // Output: { quantity: { _id: 'Option', _tag: 'None' } } console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" })) // Output: { quantity: { _id: 'Option', _tag: 'Some', value: 2 } } ``` ##### Exposed Values You can access the original schema type (before it was marked as optional) using the `from` property. **Example** (Accessing the Original Schema) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { as: "Option" }) }) // ┌─── typeof Schema.NumberFromString // ▼ const from = Product.fields.quantity.from ``` #### Optional with Exactness The `exact` option ensures that the default behavior of the optional field applies only when the field is entirely missing, not when it is `undefined`. **Syntax** ```ts showLineNumbers=false optionalWith(schema: Schema, { as: "Option", exact: true }) ``` ##### Decoding | Input | Output | | ----------------- | --------------------------------- | | `` | transforms to `Option.none()` | | `undefined` | `ParseError` | | `i: I` | transforms to `Option.some(a: A)` | ##### Encoding | Input | Output | | ------------------- | ------------------------------- | | `Option.none()` | transforms to `` | | `Option.some(a: A)` | transforms back to `i: I` | **Example** (Using Exactness with Optional Field as Option) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { as: "Option", exact: true }) }) // ┌─── { readonly quantity?: string; } // ▼ type Encoded = typeof Product.Encoded // ┌─── { readonly quantity: Option; } // ▼ type Type = typeof Product.Type console.log(Schema.decodeUnknownSync(Product)({})) // Output: { quantity: { _id: 'Option', _tag: 'None' } } console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" })) // Output: { quantity: { _id: 'Option', _tag: 'Some', value: 2 } } console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined })) /* throws: ParseError: (Struct (Encoded side) <-> Struct (Type side)) └─ Encoded side transformation failure └─ Struct (Encoded side) └─ ["quantity"] └─ NumberFromString └─ Encoded side transformation failure └─ Expected string, actual undefined */ ``` ##### Exposed Values You can access the original schema type (before it was marked as optional) using the `from` property. **Example** (Accessing the Original Schema) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { as: "Option", exact: true }) }) // ┌─── typeof Schema.NumberFromString // ▼ const from = Product.fields.quantity.from ``` #### Optional with Nullability The `nullable` option extends the default behavior to treat `null` as equivalent to `Option.none()`, alongside missing or `undefined` values. **Syntax** ```ts showLineNumbers=false optionalWith(schema: Schema, { as: "Option", nullable: true }) ``` ##### Decoding | Input | Output | | ----------------- | --------------------------------- | | `` | transforms to `Option.none()` | | `undefined` | transforms to `Option.none()` | | `null` | transforms to `Option.none()` | | `i: I` | transforms to `Option.some(a: A)` | ##### Encoding | Input | Output | | ------------------- | ------------------------------- | | `Option.none()` | transforms to `` | | `Option.some(a: A)` | transforms back to `i: I` | **Example** (Handling Null as Missing Value with Optional Field as Option) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { as: "Option", nullable: true }) }) // ┌─── { readonly quantity?: string | null | undefined; } // ▼ type Encoded = typeof Product.Encoded // ┌─── { readonly quantity: Option; } // ▼ type Type = typeof Product.Type console.log(Schema.decodeUnknownSync(Product)({})) // Output: { quantity: { _id: 'Option', _tag: 'None' } } console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined })) // Output: { quantity: { _id: 'Option', _tag: 'None' } } console.log(Schema.decodeUnknownSync(Product)({ quantity: null })) // Output: { quantity: { _id: 'Option', _tag: 'None' } } console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" })) // Output: { quantity: { _id: 'Option', _tag: 'Some', value: 2 } } ``` ##### Exposed Values You can access the original schema type (before it was marked as optional) using the `from` property. **Example** (Accessing the Original Schema) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { as: "Option", nullable: true }) }) // ┌─── typeof Schema.NumberFromString // ▼ const from = Product.fields.quantity.from ``` #### Combining Exactness and Nullability When both `exact` and `nullable` options are used together, only `null` and missing fields are treated as `Option.none()`, while `undefined` is considered an invalid value. **Syntax** ```ts showLineNumbers=false optionalWith(schema: Schema, { as: "Option", exact: true, nullable: true }) ``` ##### Decoding | Input | Output | | ----------------- | --------------------------------- | | `` | transforms to `Option.none()` | | `undefined` | `ParseError` | | `null` | transforms to `Option.none()` | | `i: I` | transforms to `Option.some(a: A)` | ##### Encoding | Input | Output | | ------------------- | ------------------------------- | | `Option.none()` | transforms to `` | | `Option.some(a: A)` | transforms back to `i: I` | **Example** (Using Exactness and Handling Null as Missing Value with Optional Field as Option) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { as: "Option", exact: true, nullable: true }) }) // ┌─── { readonly quantity?: string | null; } // ▼ type Encoded = typeof Product.Encoded // ┌─── { readonly quantity: Option; } // ▼ type Type = typeof Product.Type console.log(Schema.decodeUnknownSync(Product)({})) // Output: { quantity: { _id: 'Option', _tag: 'None' } } console.log(Schema.decodeUnknownSync(Product)({ quantity: null })) // Output: { quantity: { _id: 'Option', _tag: 'None' } } console.log(Schema.decodeUnknownSync(Product)({ quantity: "2" })) // Output: { quantity: { _id: 'Option', _tag: 'Some', value: 2 } } console.log(Schema.decodeUnknownSync(Product)({ quantity: undefined })) /* throws: ParseError: (Struct (Encoded side) <-> Struct (Type side)) └─ Encoded side transformation failure └─ Struct (Encoded side) └─ ["quantity"] └─ NumberFromString └─ Encoded side transformation failure └─ Expected string, actual undefined */ ``` ##### Exposed Values You can access the original schema type (before it was marked as optional) using the `from` property. **Example** (Accessing the Original Schema) ```ts twoslash import { Schema } from "effect" const Product = Schema.Struct({ quantity: Schema.optionalWith(Schema.NumberFromString, { as: "Option", exact: true, nullable: true }) }) // ┌─── typeof Schema.NumberFromString // ▼ const from = Product.fields.quantity.from ``` ## Optional Fields Primitives ### optionalToOptional The `Schema.optionalToOptional` API allows you to manage transformations from an optional field in the input to an optional field in the output. This can be useful for controlling both the output type and whether a field is present or absent based on specific criteria. One common use case for `optionalToOptional` is handling fields where a specific input value, such as an empty string, should be treated as an absent field in the output. **Syntax** ```ts showLineNumbers=false const optionalToOptional = ( from: Schema, to: Schema, options: { readonly decode: (o: Option.Option) => Option.Option, readonly encode: (o: Option.Option) => Option.Option } ): PropertySignature<"?:", TA, never, "?:", FI, false, FR | TR> ``` In this function: - The `from` parameter specifies the input schema, and `to` specifies the output schema. - The `decode` and `encode` functions define how the field should be interpreted on both sides: - `Option.none()` as an input argument indicates a missing field in the input. - Returning `Option.none()` from either function will omit the field in the output. **Example** (Omitting Empty Strings from the Output) Consider an optional field of type `string` where empty strings in the input should be removed from the output. ```ts twoslash import { Option, Schema } from "effect" const schema = Schema.Struct({ nonEmpty: Schema.optionalToOptional(Schema.String, Schema.String, { // ┌─── Option // ▼ decode: (maybeString) => { if (Option.isNone(maybeString)) { // If `maybeString` is `None`, the field is absent in the input. // Return Option.none() to omit it in the output. return Option.none() } // Extract the value from the `Some` instance const value = maybeString.value if (value === "") { // Treat empty strings as missing in the output // by returning Option.none(). return Option.none() } // Include non-empty strings in the output. return Option.some(value) }, // In the encoding phase, you can decide to process the field // similarly to the decoding phase or use a different logic. // Here, the logic is left unchanged. // // ┌─── Option // ▼ encode: (maybeString) => maybeString }) }) // Decoding examples const decode = Schema.decodeUnknownSync(schema) console.log(decode({})) // Output: {} console.log(decode({ nonEmpty: "" })) // Output: {} console.log(decode({ nonEmpty: "a non-empty string" })) // Output: { nonEmpty: 'a non-empty string' } // Encoding examples const encode = Schema.encodeSync(schema) console.log(encode({})) // Output: {} console.log(encode({ nonEmpty: "" })) // Output: { nonEmpty: '' } console.log(encode({ nonEmpty: "a non-empty string" })) // Output: { nonEmpty: 'a non-empty string' } ``` You can simplify the decoding logic with `Option.filter`, which filters out unwanted values in a concise way. **Example** (Using `Option.filter` for Decoding) ```ts twoslash import { identity, Option, Schema } from "effect" const schema = Schema.Struct({ nonEmpty: Schema.optionalToOptional(Schema.String, Schema.String, { decode: Option.filter((s) => s !== ""), encode: identity }) }) ``` ### optionalToRequired The `Schema.optionalToRequired` API lets you transform an optional field into a required one, with custom logic to handle cases when the field is missing in the input. **Syntax** ```ts showLineNumbers=false const optionalToRequired = ( from: Schema, to: Schema, options: { readonly decode: (o: Option.Option) => TI, readonly encode: (ti: TI) => Option.Option } ): PropertySignature<":", TA, never, "?:", FI, false, FR | TR> ``` In this function: - `from` specifies the input schema, while `to` specifies the output schema. - The `decode` and `encode` functions define the transformation behavior: - Passing `Option.none()` to `decode` means the field is absent in the input. The function can then return a default value for the output. - Returning `Option.none()` in `encode` will omit the field in the output. **Example** (Setting `null` as Default for Missing Field) This example demonstrates how to use `optionalToRequired` to provide a `null` default value when the `nullable` field is missing in the input. During encoding, fields with a value of `null` are omitted from the output. ```ts twoslash import { Option, Schema } from "effect" const schema = Schema.Struct({ nullable: Schema.optionalToRequired( // Input schema for an optional string Schema.String, // Output schema allowing null or string Schema.NullOr(Schema.String), { // ┌─── Option // ▼ decode: (maybeString) => { if (Option.isNone(maybeString)) { // If `maybeString` is `None`, the field is absent in the input. // Return `null` as the default value for the output. return null } // Extract the value from the `Some` instance // and use it as the output. return maybeString.value }, // During encoding, treat `null` as an absent field // // ┌─── string | null // ▼ encode: (stringOrNull) => stringOrNull === null ? // Omit the field by returning `None` Option.none() : // Include the field by returning `Some` Option.some(stringOrNull) } ) }) // Decoding examples const decode = Schema.decodeUnknownSync(schema) console.log(decode({})) // Output: { nullable: null } console.log(decode({ nullable: "a value" })) // Output: { nullable: 'a value' } // Encoding examples const encode = Schema.encodeSync(schema) console.log(encode({ nullable: "a value" })) // Output: { nullable: 'a value' } console.log(encode({ nullable: null })) // Output: {} ``` You can streamline the decoding and encoding logic using `Option.getOrElse` and `Option.liftPredicate` for concise and readable transformations. **Example** (Using `Option.getOrElse` and `Option.liftPredicate`) ```ts twoslash import { Option, Schema } from "effect" const schema = Schema.Struct({ nullable: Schema.optionalToRequired( Schema.String, Schema.NullOr(Schema.String), { decode: Option.getOrElse(() => null), encode: Option.liftPredicate((value) => value !== null) } ) }) ``` ### requiredToOptional The `requiredToOptional` API allows you to transform a required field into an optional one, applying custom logic to determine when the field can be omitted. **Syntax** ```ts showLineNumbers=false const requiredToOptional = ( from: Schema, to: Schema, options: { readonly decode: (fa: FA) => Option.Option readonly encode: (o: Option.Option) => FA } ): PropertySignature<"?:", TA, never, ":", FI, false, FR | TR> ``` With `decode` and `encode` functions, you control the presence or absence of the field: - `Option.none()` as an argument in `decode` means the field is missing in the input. - `Option.none()` as a return value from `encode` means the field will be omitted in the output. **Example** (Handling Empty String as Missing Value) In this example, the `name` field is required but treated as optional if it is an empty string. During decoding, an empty string in `name` is considered absent, while encoding ensures a value (using an empty string as a default if `name` is absent). ```ts twoslash import { Option, Schema } from "effect" const schema = Schema.Struct({ name: Schema.requiredToOptional(Schema.String, Schema.String, { // ┌─── string // ▼ decode: (string) => { // Treat empty string as a missing value if (string === "") { // Omit the field by returning `None` return Option.none() } // Otherwise, return the string as is return Option.some(string) }, // ┌─── Option // ▼ encode: (maybeString) => { // Check if the field is missing if (Option.isNone(maybeString)) { // Provide an empty string as default return "" } // Otherwise, return the string as is return maybeString.value } }) }) // Decoding examples const decode = Schema.decodeUnknownSync(schema) console.log(decode({ name: "John" })) // Output: { name: 'John' } console.log(decode({ name: "" })) // Output: {} // Encoding examples const encode = Schema.encodeSync(schema) console.log(encode({ name: "John" })) // Output: { name: 'John' } console.log(encode({})) // Output: { name: '' } ``` You can streamline the decoding and encoding logic using `Option.liftPredicate` and `Option.getOrElse` for concise and readable transformations. **Example** (Using `Option.liftPredicate` and `Option.getOrElse`) ```ts twoslash import { Option, Schema } from "effect" const schema = Schema.Struct({ name: Schema.requiredToOptional(Schema.String, Schema.String, { decode: Option.liftPredicate((s) => s !== ""), encode: Option.getOrElse(() => "") }) }) ``` ## Extending Schemas Schemas in `effect` can be extended in multiple ways, allowing you to combine or enhance existing types with additional fields or functionality. One common method is to use the `fields` property available in `Struct` schemas. This property provides a convenient way to add fields or merge fields from different structs while retaining the original `Struct` type. This approach also makes it easier to access and modify fields. For more complex cases, such as extending a struct with a union, you may want to use the `Schema.extend` function, which offers flexibility in scenarios where direct field spreading may not be sufficient. ### Spreading Struct fields Structs provide access to their fields through the `fields` property, which allows you to extend an existing struct by adding additional fields or combining fields from multiple structs. **Example** (Adding New Fields) ```ts twoslash import { Schema } from "effect" const Original = Schema.Struct({ a: Schema.String, b: Schema.String }) const Extended = Schema.Struct({ ...Original.fields, // Adding new fields c: Schema.String, d: Schema.String }) // ┌─── { // | readonly a: string; // | readonly b: string; // | readonly c: string; // | readonly d: string; // | } // ▼ type Type = typeof Extended.Type ``` **Example** (Adding Additional Index Signatures) ```ts twoslash import { Schema } from "effect" const Original = Schema.Struct({ a: Schema.String, b: Schema.String }) const Extended = Schema.Struct( Original.fields, // Adding an index signature Schema.Record({ key: Schema.String, value: Schema.String }) ) // ┌─── { // │ readonly [x: string]: string; // | readonly a: string; // | readonly b: string; // | } // ▼ type Type = typeof Extended.Type ``` **Example** (Combining Fields from Multiple Structs) ```ts twoslash import { Schema } from "effect" const Struct1 = Schema.Struct({ a: Schema.String, b: Schema.String }) const Struct2 = Schema.Struct({ c: Schema.String, d: Schema.String }) const Extended = Schema.Struct({ ...Struct1.fields, ...Struct2.fields }) // ┌─── { // | readonly a: string; // | readonly b: string; // | readonly c: string; // | readonly d: string; // | } // ▼ type Type = typeof Extended.Type ``` ### The extend function The `Schema.extend` function provides a structured method to expand schemas, especially useful when direct [field spreading](#spreading-struct-fields) isn't sufficient—such as when you need to extend a struct with a union of other structs. Supported extensions include: - `Schema.String` with another `Schema.String` refinement or a string literal - `Schema.Number` with another `Schema.Number` refinement or a number literal - `Schema.Boolean` with another `Schema.Boolean` refinement or a boolean literal - A struct with another struct where overlapping fields support extension - A struct with in index signature - A struct with a union of supported schemas - A refinement of a struct with a supported schema - A `suspend` of a struct with a supported schema - A transformation between structs where the "from" and "to" sides have no overlapping fields with the target struct **Example** (Extending a Struct with a Union of Structs) ```ts twoslash import { Schema } from "effect" const Struct = Schema.Struct({ a: Schema.String }) const UnionOfStructs = Schema.Union( Schema.Struct({ b: Schema.String }), Schema.Struct({ c: Schema.String }) ) const Extended = Schema.extend(Struct, UnionOfStructs) // ┌─── { // | readonly a: string; // | } & ({ // | readonly b: string; // | } | { // | readonly c: string; // | }) // ▼ type Type = typeof Extended.Type ``` **Example** (Attempting to Extend Structs with Conflicting Fields) This example demonstrates an attempt to extend a struct with another struct that contains overlapping field names, resulting in an error due to conflicting types. ```ts twoslash import { Schema } from "effect" const Struct = Schema.Struct({ a: Schema.String }) const OverlappingUnion = Schema.Union( Schema.Struct({ a: Schema.Number }), // conflicting type for key "a" Schema.Struct({ d: Schema.String }) ) const Extended = Schema.extend(Struct, OverlappingUnion) /* throws: Error: Unsupported schema or overlapping types at path: ["a"] details: cannot extend string with number */ ``` **Example** (Extending a Refinement with Another Refinement) In this example, we extend two refinements, `Integer` and `Positive`, creating a schema that enforces both integer and positivity constraints. ```ts twoslash import { Schema } from "effect" const Integer = Schema.Int.pipe(Schema.brand("Int")) const Positive = Schema.Positive.pipe(Schema.brand("Positive")) // ┌─── Schema & Brand<"Int">, number, never> // ▼ const PositiveInteger = Schema.asSchema(Schema.extend(Positive, Integer)) Schema.decodeUnknownSync(PositiveInteger)(-1) /* throws ParseError: positive & Brand<"Positive"> & int & Brand<"Int"> └─ From side refinement failure └─ positive & Brand<"Positive"> └─ Predicate refinement failure └─ Expected a positive number, actual -1 */ Schema.decodeUnknownSync(PositiveInteger)(1.1) /* throws ParseError: positive & Brand<"Positive"> & int & Brand<"Int"> └─ Predicate refinement failure └─ Expected an integer, actual 1.1 */ ``` ## Renaming Properties ### Renaming a Property During Definition To rename a property directly during schema creation, you can utilize the `Schema.fromKey` function. **Example** (Renaming a Required Property) ```ts twoslash import { Schema } from "effect" const schema = Schema.Struct({ a: Schema.propertySignature(Schema.String).pipe(Schema.fromKey("c")), b: Schema.Number }) // ┌─── { readonly c: string; readonly b: number; } // ▼ type Encoded = typeof schema.Encoded // ┌─── { readonly a: string; readonly b: number; } // ▼ type Type = typeof schema.Type console.log(Schema.decodeUnknownSync(schema)({ c: "c", b: 1 })) // Output: { a: "c", b: 1 } ``` **Example** (Renaming an Optional Property) ```ts twoslash import { Schema } from "effect" const schema = Schema.Struct({ a: Schema.optional(Schema.String).pipe(Schema.fromKey("c")), b: Schema.Number }) // ┌─── { readonly b: number; readonly c?: string | undefined; } // ▼ type Encoded = typeof schema.Encoded // ┌─── { readonly a?: string | undefined; readonly b: number; } // ▼ type Type = typeof schema.Type console.log(Schema.decodeUnknownSync(schema)({ c: "c", b: 1 })) // Output: { a: 'c', b: 1 } console.log(Schema.decodeUnknownSync(schema)({ b: 1 })) // Output: { b: 1 } ``` Using `Schema.optional` automatically returns a `PropertySignature`, making it unnecessary to explicitly use `Schema.propertySignature` as required for renaming required fields in the previous example. ### Renaming Properties of an Existing Schema For existing schemas, the `Schema.rename` API offers a way to systematically change property names across a schema, even within complex structures like unions, though in case of structs you lose the original field types. **Example** (Renaming Properties in a Struct Schema) ```ts twoslash import { Schema } from "effect" const Original = Schema.Struct({ c: Schema.String, b: Schema.Number }) // Renaming the "c" property to "a" // // // ┌─── SchemaClass<{ // | readonly a: string; // | readonly b: number; // | }> // ▼ const Renamed = Schema.rename(Original, { c: "a" }) console.log(Schema.decodeUnknownSync(Renamed)({ c: "c", b: 1 })) // Output: { a: "c", b: 1 } ``` **Example** (Renaming Properties in Union Schemas) ```ts twoslash import { Schema } from "effect" const Original = Schema.Union( Schema.Struct({ c: Schema.String, b: Schema.Number }), Schema.Struct({ c: Schema.String, d: Schema.Boolean }) ) // Renaming the "c" property to "a" for all members // // ┌─── SchemaClass<{ // | readonly a: string; // | readonly b: number; // | } | { // | readonly a: string; // | readonly d: number; // | }> // ▼ const Renamed = Schema.rename(Original, { c: "a" }) console.log(Schema.decodeUnknownSync(Renamed)({ c: "c", b: 1 })) // Output: { a: "c", b: 1 } console.log(Schema.decodeUnknownSync(Renamed)({ c: "c", d: false })) // Output: { a: 'c', d: false } ``` ## Recursive Schemas The `Schema.suspend` function is designed for defining schemas that reference themselves, such as in recursive data structures. **Example** (Self-Referencing Schema) In this example, the `Category` schema references itself through the `subcategories` field, which is an array of `Category` objects. ```ts twoslash import { Schema } from "effect" interface Category { readonly name: string readonly subcategories: ReadonlyArray } const Category = Schema.Struct({ name: Schema.String, subcategories: Schema.Array( Schema.suspend((): Schema.Schema => Category) ) }) ``` **Example** (Type Inference Error) ```ts twoslash import { Schema } from "effect" // @ts-expect-error const Category = Schema.Struct({ name: Schema.String, // @ts-expect-error subcategories: Schema.Array(Schema.suspend(() => Category)) }) /* 'Category' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.ts(7022) */ ``` ### A Helpful Pattern to Simplify Schema Definition As we've observed, it's necessary to define an interface for the `Type` of the schema to enable recursive schema definition, which can complicate things and be quite tedious. One pattern to mitigate this is to **separate the field responsible for recursion** from all other fields. **Example** (Separating Recursive Fields) ```ts twoslash import { Schema } from "effect" const fields = { name: Schema.String // ...other fields as needed } // Define an interface for the Category schema, // extending the Type of the defined fields interface Category extends Schema.Struct.Type { // Define `subcategories` using recursion readonly subcategories: ReadonlyArray } const Category = Schema.Struct({ ...fields, // Spread in the base fields subcategories: Schema.Array( // Define `subcategories` using recursion Schema.suspend((): Schema.Schema => Category) ) }) ``` ### Mutually Recursive Schemas You can also use `Schema.suspend` to create mutually recursive schemas, where two schemas reference each other. In the following example, `Expression` and `Operation` form a simple arithmetic expression tree by referencing each other. **Example** (Defining Mutually Recursive Schemas) ```ts twoslash import { Schema } from "effect" interface Expression { readonly type: "expression" readonly value: number | Operation } interface Operation { readonly type: "operation" readonly operator: "+" | "-" readonly left: Expression readonly right: Expression } const Expression = Schema.Struct({ type: Schema.Literal("expression"), value: Schema.Union( Schema.Number, Schema.suspend((): Schema.Schema => Operation) ) }) const Operation = Schema.Struct({ type: Schema.Literal("operation"), operator: Schema.Literal("+", "-"), left: Expression, right: Expression }) ``` ### Recursive Types with Different Encoded and Type Defining a recursive schema where the `Encoded` type differs from the `Type` type adds another layer of complexity. In such cases, we need to define two interfaces: one for the `Type` type, as seen previously, and another for the `Encoded` type. **Example** (Recursive Schema with Different Encoded and Type Definitions) Let's consider an example: suppose we want to add an `id` field to the `Category` schema, where the schema for `id` is `NumberFromString`. It's important to note that `NumberFromString` is a schema that transforms a string into a number, so the `Type` and `Encoded` types of `NumberFromString` differ, being `number` and `string` respectively. When we add this field to the `Category` schema, TypeScript raises an error: ```ts twoslash import { Schema } from "effect" const fields = { id: Schema.NumberFromString, name: Schema.String } interface Category extends Schema.Struct.Type { readonly subcategories: ReadonlyArray } const Category = Schema.Struct({ ...fields, subcategories: Schema.Array( // @ts-expect-error Schema.suspend((): Schema.Schema => Category) ) }) /* Type 'Struct<{ subcategories: Array$>; id: typeof NumberFromString; name: typeof String$; }>' is not assignable to type 'Schema'. The types of 'Encoded.id' are incompatible between these types. Type 'string' is not assignable to type 'number'.ts(2322) */ ``` This error occurs because the explicit annotation `Schema.Schema` is no longer sufficient and needs to be adjusted by explicitly adding the `Encoded` type: ```ts twoslash import { Schema } from "effect" const fields = { id: Schema.NumberFromString, name: Schema.String } interface Category extends Schema.Struct.Type { readonly subcategories: ReadonlyArray } interface CategoryEncoded extends Schema.Struct.Encoded { readonly subcategories: ReadonlyArray } const Category = Schema.Struct({ ...fields, subcategories: Schema.Array( Schema.suspend( (): Schema.Schema => Category ) ) }) ``` # [Schema to Arbitrary](https://effect.website/docs/schema/arbitrary/) ## Overview import { Aside } from "@astrojs/starlight/components" The `Arbitrary.make` function allows for the creation of random values that align with a specific `Schema`. This function returns an `Arbitrary` from the [fast-check](https://github.com/dubzzz/fast-check) library, which is particularly useful for generating random test data that adheres to the defined schema constraints. **Example** (Generating Arbitrary Data for a Schema) ```ts twoslash import { Arbitrary, FastCheck, Schema } from "effect" // Define a Person schema with constraints const Person = Schema.Struct({ name: Schema.NonEmptyString, age: Schema.Int.pipe(Schema.between(1, 80)) }) // Create an Arbitrary based on the schema const arb = Arbitrary.make(Person) // Generate random samples from the Arbitrary console.log(FastCheck.sample(arb, 2)) /* Example Output: [ { name: 'q r', age: 3 }, { name: '&|', age: 6 } ] */ ``` To make the output more realistic, see the [Customizing Arbitrary Data Generation](#customizing-arbitrary-data-generation) section. ## Filters When generating random values, `Arbitrary` tries to follow the schema's constraints. It uses the most appropriate `fast-check` primitives and applies constraints if the primitive supports them. For instance, if you define an `age` property as: ```ts showLineNumbers=false Schema.Int.pipe(Schema.between(1, 80)) ``` the arbitrary generation will use: ```ts showLineNumbers=false FastCheck.integer({ min: 1, max: 80 }) ``` to produce values within that range. ### Patterns To generate efficient arbitraries for strings that must match a certain pattern, use the `Schema.pattern` filter instead of writing a custom filter: **Example** (Using `Schema.pattern` for Pattern Constraints) ```ts twoslash import { Schema } from "effect" // ❌ Without using Schema.pattern (less efficient) const Bad = Schema.String.pipe(Schema.filter((s) => /^[a-z]+$/.test(s))) // ✅ Using Schema.pattern (more efficient) const Good = Schema.String.pipe(Schema.pattern(/^[a-z]+$/)) ``` By using `Schema.pattern`, arbitrary generation will rely on `FastCheck.stringMatching(regexp)`, which is more efficient and directly aligned with the defined pattern. When multiple patterns are used, they are combined into a union. For example: ```ts (?:${pattern1})|(?:${pattern2}) ``` This approach ensures all patterns have an equal chance of generating values when using `FastCheck.stringMatching`. ## Transformations and Arbitrary Generation When generating arbitrary data, it is important to understand how transformations and filters are handled within a schema: **Example** (Filters and Transformations) ```ts twoslash import { Arbitrary, FastCheck, Schema } from "effect" // Schema with filters before the transformation const schema1 = Schema.compose(Schema.NonEmptyString, Schema.Trim).pipe( Schema.maxLength(500) ) // May produce empty strings due to ignored NonEmpty filter console.log(FastCheck.sample(Arbitrary.make(schema1), 2)) /* Example Output: [ '', '"Ry' ] */ // Schema with filters applied after transformations const schema2 = Schema.Trim.pipe( Schema.nonEmptyString(), Schema.maxLength(500) ) // Adheres to all filters, avoiding empty strings console.log(FastCheck.sample(Arbitrary.make(schema2), 2)) /* Example Output: [ ']H+MPXgZKz', 'SNS|waP~\\' ] */ ``` **Explanation:** - `schema1`: Takes into account `Schema.maxLength(500)` since it is applied after the `Schema.Trim` transformation, but ignores the `Schema.NonEmptyString` as it precedes the transformations. - `schema2`: Adheres fully to all filters because they are correctly sequenced after transformations, preventing the generation of undesired data. ### Best Practices To ensure consistent and valid arbitrary data generation, follow these guidelines: 1. **Apply Filters First**: Define filters for the initial type (`I`). 2. **Apply Transformations**: Add transformations to convert the data. 3. **Apply Final Filters**: Use filters for the transformed type (`A`). This setup ensures that each stage of data processing is precise and well-defined. **Example** (Avoid Mixed Filters and Transformations) Avoid haphazard combinations of transformations and filters: ```ts twoslash import { Schema } from "effect" // Less optimal approach: Mixing transformations and filters const problematic = Schema.compose(Schema.Lowercase, Schema.Trim) ``` Prefer a structured approach by separating transformation steps from filter applications: **Example** (Preferred Structured Approach) ```ts twoslash import { Schema } from "effect" // Recommended: Separate transformations and filters const improved = Schema.transform( Schema.String, Schema.String.pipe(Schema.trimmed(), Schema.lowercased()), { strict: true, decode: (s) => s.trim().toLowerCase(), encode: (s) => s } ) ``` ## Customizing Arbitrary Data Generation You can customize how arbitrary data is generated using the `arbitrary` annotation in schema definitions. **Example** (Custom Arbitrary Generator) ```ts twoslash import { Arbitrary, FastCheck, Schema } from "effect" const Name = Schema.NonEmptyString.annotations({ arbitrary: () => (fc) => fc.constantFrom("Alice Johnson", "Dante Howell", "Marta Reyes") }) const Age = Schema.Int.pipe(Schema.between(1, 80)) const Person = Schema.Struct({ name: Name, age: Age }) const arb = Arbitrary.make(Person) console.log(FastCheck.sample(arb, 2)) /* Example Output: [ { name: 'Dante Howell', age: 6 }, { name: 'Marta Reyes', age: 53 } ] */ ``` The annotation allows access the complete export of the fast-check library (`fc`). This setup enables you to return an `Arbitrary` that precisely generates the type of data desired. ### Integration with Fake Data Generators When using mocking libraries like [@faker-js/faker](https://www.npmjs.com/package/@faker-js/faker), you can combine them with `fast-check` to generate realistic data for testing purposes. **Example** (Integrating with Faker) ```ts twoslash import { Arbitrary, FastCheck, Schema } from "effect" import { faker } from "@faker-js/faker" const Name = Schema.NonEmptyString.annotations({ arbitrary: () => (fc) => fc.constant(null).map(() => { // Each time the arbitrary is sampled, faker generates a new name return faker.person.fullName() }) }) const Age = Schema.Int.pipe(Schema.between(1, 80)) const Person = Schema.Struct({ name: Name, age: Age }) const arb = Arbitrary.make(Person) console.log(FastCheck.sample(arb, 2)) /* Example Output: [ { name: 'Henry Dietrich', age: 68 }, { name: 'Lucas Haag', age: 52 } ] */ ``` # [Schema Annotations](https://effect.website/docs/schema/annotations/) ## Overview One of the key features of the Schema design is its flexibility and ability to be customized. This is achieved through "annotations." Each node in the `ast` field of a schema has an `annotations: Record` field, which allows you to attach additional information to the schema. You can manage these annotations using the `annotations` method or the `Schema.annotations` API. **Example** (Using Annotations to Customize Schema) ```ts twoslash import { Schema } from "effect" // Define a Password schema, starting with a string type const Password = Schema.String // Add a custom error message for non-string values .annotations({ message: () => "not a string" }) .pipe( // Enforce non-empty strings and provide a custom error message Schema.nonEmptyString({ message: () => "required" }), // Restrict the string length to 10 characters or fewer // with a custom error message for exceeding length Schema.maxLength(10, { message: (issue) => `${issue.actual} is too long` }) ) .annotations({ // Add a unique identifier for the schema identifier: "Password", // Provide a title for the schema title: "password", // Include a description explaining what this schema represents description: "A password is a secret string used to authenticate a user", // Add examples for better clarity examples: ["1Ki77y", "jelly22fi$h"], // Include any additional documentation documentation: `...technical information on Password schema...` }) ``` ## Built-in Annotations The following table provides an overview of common built-in annotations and their uses: | Annotation | Description | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `identifier` | Assigns a unique identifier to the schema, ideal for TypeScript identifiers and code generation purposes. Commonly used in tools like [TreeFormatter](/docs/schema/error-formatters/#customizing-the-output) to clarify output. Examples include `"Person"`, `"Product"`. | | `title` | Sets a short, descriptive title for the schema, similar to a JSON Schema title. Useful for documentation or UI headings. It is also used by [TreeFormatter](/docs/schema/error-formatters/#customizing-the-output) to enhance readability of error messages. | | `description` | Provides a detailed explanation about the schema's purpose, akin to a JSON Schema description. Used by [TreeFormatter](/docs/schema/error-formatters/#customizing-the-output) to provide more detailed error messages. | | `documentation` | Extends detailed documentation for the schema, beneficial for developers or automated documentation generation. | | `examples` | Lists examples of valid schema values, akin to the examples attribute in JSON Schema, useful for documentation and validation testing. | | `default` | Defines a default value for the schema, similar to the default attribute in JSON Schema, to ensure schemas are pre-populated where applicable. | | `message` | Customizes the error message for validation failures, improving clarity in outputs from tools like [TreeFormatter](/docs/schema/error-formatters/#customizing-the-output) and [ArrayFormatter](/docs/schema/error-formatters/#arrayformatter) during decoding or validation errors. | | `jsonSchema` | Specifies annotations that affect the generation of [JSON Schema](/docs/schema/json-schema/) documents, customizing how schemas are represented. | | `arbitrary` | Configures settings for generating [Arbitrary](/docs/schema/arbitrary/) test data. | | `pretty` | Configures settings for generating [Pretty](/docs/schema/pretty/) output. | | `equivalence` | Configures settings for evaluating data [Equivalence](/docs/schema/equivalence/). | | `concurrency` | Controls concurrency behavior, ensuring schemas perform optimally under concurrent operations. Refer to [Concurrency Annotation](#concurrency-annotation) for detailed usage. | | `batching` | Manages settings for batching operations to enhance performance when operations can be grouped. | | `parseIssueTitle` | Provides a custom title for parsing issues, enhancing error descriptions in outputs from [TreeFormatter](/docs/schema/error-formatters/#treeformatter-default). See [ParseIssueTitle Annotation](/docs/schema/error-formatters/#parseissuetitle-annotation) for more information. | | `parseOptions` | Allows overriding of parsing options at the schema level, offering granular control over parsing behaviors. See [Customizing Parsing Behavior at the Schema Level](/docs/schema/getting-started/#customizing-parsing-behavior-at-the-schema-level) for application details. | | `decodingFallback` | Provides a way to define custom fallback behaviors that trigger when decoding operations fail. Refer to [Handling Decoding Errors with Fallbacks](#handling-decoding-errors-with-fallbacks) for detailed usage. | ## Concurrency Annotation For more complex schemas like `Struct`, `Array`, or `Union` that contain multiple nested schemas, the `concurrency` annotation provides a way to control how validations are executed concurrently. ```ts showLineNumbers=false type ConcurrencyAnnotation = number | "unbounded" | "inherit" | undefined ``` Here's a shorter version presented in a table: | Value | Description | | ------------- | --------------------------------------------------------------- | | `number` | Limits the maximum number of concurrent tasks. | | `"unbounded"` | All tasks run concurrently with no limit. | | `"inherit"` | Inherits concurrency settings from the parent context. | | `undefined` | Tasks run sequentially, one after the other (default behavior). | **Example** (Sequential Execution) In this example, we define three tasks that simulate asynchronous operations with different durations. Since no concurrency is specified, the tasks are executed sequentially, one after the other. ```ts twoslash import { Schema } from "effect" import type { Duration } from "effect" import { Effect } from "effect" // Simulates an async task const item = (id: number, duration: Duration.DurationInput) => Schema.String.pipe( Schema.filterEffect(() => Effect.gen(function* () { yield* Effect.sleep(duration) console.log(`Task ${id} done`) return true }) ) ) const Sequential = Schema.Tuple( item(1, "30 millis"), item(2, "10 millis"), item(3, "20 millis") ) Effect.runPromise(Schema.decode(Sequential)(["a", "b", "c"])) /* Output: Task 1 done Task 2 done Task 3 done */ ``` **Example** (Concurrent Execution) By adding a `concurrency` annotation set to `"unbounded"`, the tasks can now run concurrently, meaning they don't wait for one another to finish before starting. This allows faster execution when multiple tasks are involved. ```ts twoslash import { Schema } from "effect" import type { Duration } from "effect" import { Effect } from "effect" // Simulates an async task const item = (id: number, duration: Duration.DurationInput) => Schema.String.pipe( Schema.filterEffect(() => Effect.gen(function* () { yield* Effect.sleep(duration) console.log(`Task ${id} done`) return true }) ) ) const Concurrent = Schema.Tuple( item(1, "30 millis"), item(2, "10 millis"), item(3, "20 millis") ).annotations({ concurrency: "unbounded" }) Effect.runPromise(Schema.decode(Concurrent)(["a", "b", "c"])) /* Output: Task 2 done Task 3 done Task 1 done */ ``` ## Handling Decoding Errors with Fallbacks The `DecodingFallbackAnnotation` allows you to handle decoding errors by providing a custom fallback logic. ```ts showLineNumbers=false type DecodingFallbackAnnotation = ( issue: ParseIssue ) => Effect ``` This annotation enables you to specify fallback behavior when decoding fails, making it possible to recover gracefully from errors. **Example** (Basic Fallback) In this basic example, when decoding fails (e.g., the input is `null`), the fallback value is returned instead of an error. ```ts twoslash import { Schema } from "effect" import { Either } from "effect" // Schema with a fallback value const schema = Schema.String.annotations({ decodingFallback: () => Either.right("") }) console.log(Schema.decodeUnknownSync(schema)("valid input")) // Output: valid input console.log(Schema.decodeUnknownSync(schema)(null)) // Output: ``` **Example** (Advanced Fallback with Logging) In this advanced example, when a decoding error occurs, the schema logs the issue and then returns a fallback value. This demonstrates how you can incorporate logging and other side effects during error handling. ```ts twoslash import { Schema } from "effect" import { Effect } from "effect" // Schema with logging and fallback const schemaWithLog = Schema.String.annotations({ decodingFallback: (issue) => Effect.gen(function* () { // Log the error issue yield* Effect.log(issue._tag) // Simulate a delay yield* Effect.sleep(10) // Return a fallback value return yield* Effect.succeed("") }) }) // Run the effectful fallback logic Effect.runPromise(Schema.decodeUnknown(schemaWithLog)(null)).then( console.log ) /* Output: timestamp=2024-07-25T13:22:37.706Z level=INFO fiber=#0 message=Type */ ``` ## Custom Annotations In addition to built-in annotations, you can define custom annotations to meet specific requirements. For instance, here's how to create a `deprecated` annotation: **Example** (Defining a Custom Annotation) ```ts twoslash import { Schema } from "effect" // Define a unique identifier for your custom annotation const DeprecatedId = Symbol.for( "some/unique/identifier/for/your/custom/annotation" ) // Apply the custom annotation to the schema const MyString = Schema.String.annotations({ [DeprecatedId]: true }) console.log(MyString) /* Output: [class SchemaClass] { ast: StringKeyword { annotations: { [Symbol(@effect/docs/schema/annotation/Title)]: 'string', [Symbol(@effect/docs/schema/annotation/Description)]: 'a string', [Symbol(some/unique/identifier/for/your/custom/annotation)]: true }, _tag: 'StringKeyword' }, ... } */ ``` To make your new custom annotation type-safe, you can use a module augmentation. In the next example, we want our custom annotation to be a boolean. **Example** (Adding Type Safety to Custom Annotations) ```ts twoslash import { Schema } from "effect" const DeprecatedId = Symbol.for( "some/unique/identifier/for/your/custom/annotation" ) // Module augmentation declare module "effect/Schema" { namespace Annotations { interface GenericSchema extends Schema { [DeprecatedId]?: boolean } } } const MyString = Schema.String.annotations({ // @ts-expect-error [DeprecatedId]: "bad value" // Type of computed property's value is 'string', // which is not assignable to type 'boolean'.ts(2418) }) ``` You can retrieve custom annotations using the `SchemaAST.getAnnotation` helper function. **Example** (Retrieving a Custom Annotation) ```ts twoslash collapse={4-16} import { SchemaAST, Schema } from "effect" import { Option } from "effect" const DeprecatedId = Symbol.for( "some/unique/identifier/for/your/custom/annotation" ) declare module "effect/Schema" { namespace Annotations { interface GenericSchema extends Schema { [DeprecatedId]?: boolean } } } const MyString = Schema.String.annotations({ [DeprecatedId]: true }) // Helper function to check if a schema is marked as deprecated const isDeprecated = (schema: Schema.Schema): boolean => SchemaAST.getAnnotation(DeprecatedId)(schema.ast).pipe( Option.getOrElse(() => false) ) console.log(isDeprecated(Schema.String)) // Output: false console.log(isDeprecated(MyString)) // Output: true ``` # [Basic Usage](https://effect.website/docs/schema/basic-usage/) ## Overview import { Aside } from "@astrojs/starlight/components" ## Primitives The Schema module provides built-in schemas for common primitive types. | Schema | Equivalent TypeScript Type | | ----------------------- | -------------------------- | | `Schema.String` | `string` | | `Schema.Number` | `number` | | `Schema.Boolean` | `boolean` | | `Schema.BigIntFromSelf` | `BigInt` | | `Schema.SymbolFromSelf` | `symbol` | | `Schema.Object` | `object` | | `Schema.Undefined` | `undefined` | | `Schema.Void` | `void` | | `Schema.Any` | `any` | | `Schema.Unknown` | `unknown` | | `Schema.Never` | `never` | **Example** (Using a Primitive Schema) ```ts twoslash import { Schema } from "effect" const schema = Schema.String // Infers the type as string // // ┌─── string // ▼ type Type = typeof schema.Type // Attempt to decode a null value, which will throw a parse error Schema.decodeUnknownSync(schema)(null) /* throws: ParseError: Expected string, actual null */ ``` ## asSchema To make it easier to work with schemas, built-in schemas are exposed with shorter, opaque types when possible. The `Schema.asSchema` function allows you to view any schema as `Schema`. **Example** (Expanding a Schema with `asSchema`) For example, while `Schema.String` is defined as a class with a type of `typeof Schema.String`, using `Schema.asSchema` provides the schema in its extended form as `Schema`. ```ts twoslash import { Schema } from "effect" // ┌─── typeof Schema.String // ▼ const schema = Schema.String // ┌─── Schema // ▼ const nomalized = Schema.asSchema(schema) ``` ## Unique Symbols You can create a schema for unique symbols using `Schema.UniqueSymbolFromSelf`. **Example** (Creating a Schema for a Unique Symbol) ```ts twoslash import { Schema } from "effect" const mySymbol = Symbol.for("mySymbol") const schema = Schema.UniqueSymbolFromSelf(mySymbol) // ┌─── typeof mySymbol // ▼ type Type = typeof schema.Type Schema.decodeUnknownSync(schema)(null) /* throws: ParseError: Expected Symbol(mySymbol), actual null */ ``` ## Literals Literal schemas represent a [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). You can use them to specify exact values that a type must have. Literals can be of the following types: - `string` - `number` - `boolean` - `null` - `bigint` **Example** (Defining Literal Schemas) ```ts twoslash import { Schema } from "effect" // Define various literal schemas Schema.Null // Same as S.Literal(null) Schema.Literal("a") // string literal Schema.Literal(1) // number literal Schema.Literal(true) // boolean literal Schema.Literal(2n) // BigInt literal ``` **Example** (Defining a Literal Schema for `"a"`) ```ts twoslash import { Schema } from "effect" // ┌─── Literal<["a"]> // ▼ const schema = Schema.Literal("a") // ┌─── "a" // ▼ type Type = typeof schema.Type console.log(Schema.decodeUnknownSync(schema)("a")) // Output: "a" console.log(Schema.decodeUnknownSync(schema)("b")) /* throws: ParseError: Expected "a", actual "b" */ ``` ### Union of Literals You can create a union of multiple literals by passing them as arguments to the `Schema.Literal` constructor: **Example** (Defining a Union of Literals) ```ts twoslash import { Schema } from "effect" // ┌─── Literal<["a", "b", "c"]> // ▼ const schema = Schema.Literal("a", "b", "c") // ┌─── "a" | "b" | "c" // ▼ type Type = typeof schema.Type Schema.decodeUnknownSync(schema)(null) /* throws: ParseError: "a" | "b" | "c" ├─ Expected "a", actual null ├─ Expected "b", actual null └─ Expected "c", actual null */ ``` If you want to set a custom error message for the entire union of literals, you can use the `override: true` option (see [Custom Error Messages](/docs/schema/error-messages/#custom-error-messages) for more details) to specify a unified message. **Example** (Adding a Custom Message to a Union of Literals) ```ts twoslash import { Schema } from "effect" // Schema with individual messages for each literal const individualMessages = Schema.Literal("a", "b", "c") console.log(Schema.decodeUnknownSync(individualMessages)(null)) /* throws: ParseError: "a" | "b" | "c" ├─ Expected "a", actual null ├─ Expected "b", actual null └─ Expected "c", actual null */ // Schema with a unified custom message for all literals const unifiedMessage = Schema.Literal("a", "b", "c").annotations({ message: () => ({ message: "Not a valid code", override: true }) }) console.log(Schema.decodeUnknownSync(unifiedMessage)(null)) /* throws: ParseError: Not a valid code */ ``` ### Exposed Values You can access the literals defined in a literal schema using the `literals` property: ```ts twoslash import { Schema } from "effect" const schema = Schema.Literal("a", "b", "c") // ┌─── readonly ["a", "b", "c"] // ▼ const literals = schema.literals ``` ### The pickLiteral Utility You can use `Schema.pickLiteral` with a literal schema to narrow down its possible values. **Example** (Using `pickLiteral` to Narrow Values) ```ts twoslash import { Schema } from "effect" // Create a schema for a subset of literals ("a" and "b") from a larger set // // ┌─── Literal<["a", "b"]> // ▼ const schema = Schema.Literal("a", "b", "c").pipe( Schema.pickLiteral("a", "b") ) ``` Sometimes, you may need to reuse a literal schema in other parts of your code. Below is an example demonstrating how to do this: **Example** (Creating a Subtype from a Literal Schema) ```ts twoslash import { Schema } from "effect" // Define the base set of fruit categories const FruitCategory = Schema.Literal("sweet", "citrus", "tropical") // Define a general Fruit schema with the base category set const Fruit = Schema.Struct({ id: Schema.Number, category: FruitCategory }) // Define a specific Fruit schema for only "sweet" and "citrus" categories const SweetAndCitrusFruit = Schema.Struct({ id: Schema.Number, category: FruitCategory.pipe(Schema.pickLiteral("sweet", "citrus")) }) ``` In this example, `FruitCategory` serves as the source of truth for the different fruit categories. We reuse it to create a subtype of `Fruit` called `SweetAndCitrusFruit`, ensuring that only the specified categories (`"sweet"` and `"citrus"`) are allowed. This approach helps maintain consistency throughout your code and provides type safety if the category definition changes. ## Template literals In TypeScript, [template literals types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html) allow you to embed expressions within string literals. The `Schema.TemplateLiteral` constructor allows you to create a schema for these template literal types. **Example** (Defining Template Literals) ```ts twoslash import { Schema } from "effect" // This creates a schema for: `a${string}` // // ┌─── TemplateLiteral<`a${string}`> // ▼ const schema1 = Schema.TemplateLiteral("a", Schema.String) // This creates a schema for: // `https://${string}.com` | `https://${string}.net` const schema2 = Schema.TemplateLiteral( "https://", Schema.String, ".", Schema.Literal("com", "net") ) ``` **Example** (From [template literals types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html) Documentation) Let's look at a more complex example. Suppose you have two sets of locale IDs for emails and footers. You can use the `Schema.TemplateLiteral` constructor to create a schema that combines these IDs: ```ts twoslash import { Schema } from "effect" const EmailLocaleIDs = Schema.Literal("welcome_email", "email_heading") const FooterLocaleIDs = Schema.Literal("footer_title", "footer_sendoff") // This creates a schema for: // "welcome_email_id" | "email_heading_id" | // "footer_title_id" | "footer_sendoff_id" const schema = Schema.TemplateLiteral( Schema.Union(EmailLocaleIDs, FooterLocaleIDs), "_id" ) ``` ### Supported Span Types The `Schema.TemplateLiteral` constructor supports the following types of spans: - `Schema.String` - `Schema.Number` - Literals: `string | number | boolean | null | bigint`. These can be either wrapped by `Schema.Literal` or used directly - Unions of the above types - Brands of the above types **Example** (Using a Branded String in a Template Literal) ```ts twoslash import { Schema } from "effect" // Create a branded string schema for an authorization token const AuthorizationToken = Schema.String.pipe( Schema.brand("AuthorizationToken") ) // This creates a schema for: // `Bearer ${string & Brand<"AuthorizationToken">}` const schema = Schema.TemplateLiteral("Bearer ", AuthorizationToken) ``` ### TemplateLiteralParser The `Schema.TemplateLiteral` constructor, while useful as a simple validator, only verifies that an input conforms to a specific string pattern by converting template literal definitions into regular expressions. Similarly, [`Schema.pattern`](/docs/schema/filters/#string-filters) employs regular expressions directly for the same purpose. Post-validation, both methods require additional manual parsing to convert the validated string into a usable data format. To address these limitations and eliminate the need for manual post-validation parsing, the `Schema.TemplateLiteralParser` API has been developed. It not only validates the input format but also automatically parses it into a more structured and type-safe output, specifically into a **tuple** format. The `Schema.TemplateLiteralParser` constructor supports the same types of [spans](#supported-span-types) as `Schema.TemplateLiteral`. **Example** (Using TemplateLiteralParser for Parsing and Encoding) ```ts twoslash import { Schema } from "effect" // ┌─── Schema // ▼ const schema = Schema.TemplateLiteralParser( Schema.NumberFromString, "a", Schema.NonEmptyString ) console.log(Schema.decodeSync(schema)("100afoo")) // Output: [ 100, 'a', 'foo' ] console.log(Schema.encodeSync(schema)([100, "a", "foo"])) // Output: '100afoo' ``` ## Native enums The Schema module provides support for native TypeScript enums. You can define a schema for an enum using `Schema.Enums`, allowing you to validate values that belong to the enum. **Example** (Defining a Schema for an Enum) ```ts twoslash import { Schema } from "effect" enum Fruits { Apple, Banana } // ┌─── Enums // ▼ const schema = Schema.Enums(Fruits) // // ┌─── Fruits // ▼ type Type = typeof schema.Type ``` ### Exposed Values Enums are accessible through the `enums` property of the schema. You can use this property to retrieve individual members or the entire set of enum values. ```ts twoslash import { Schema } from "effect" enum Fruits { Apple, Banana } const schema = Schema.Enums(Fruits) schema.enums // Returns all enum members schema.enums.Apple // Access the Apple member schema.enums.Banana // Access the Banana member ``` ## Unions The Schema module includes a built-in `Schema.Union` constructor for creating "OR" types, allowing you to define schemas that can represent multiple types. **Example** (Defining a Union Schema) ```ts twoslash import { Schema } from "effect" // ┌─── Union<[typeof Schema.String, typeof Schema.Number]> // ▼ const schema = Schema.Union(Schema.String, Schema.Number) // ┌─── string | number // ▼ type Type = typeof schema.Type ``` ### Union Member Evaluation Order When decoding, union members are evaluated in the order they are defined. If a value matches the first member, it will be decoded using that schema. If not, the decoding process moves on to the next member. If multiple schemas could decode the same value, the order matters. Placing a more general schema before a more specific one may result in missing properties, as the first matching schema will be used. **Example** (Handling Overlapping Schemas in a Union) ```ts twoslash import { Schema } from "effect" // Define two overlapping schemas const Member1 = Schema.Struct({ a: Schema.String }) const Member2 = Schema.Struct({ a: Schema.String, b: Schema.Number }) // ❌ Define a union where Member1 appears first const Bad = Schema.Union(Member1, Member2) console.log(Schema.decodeUnknownSync(Bad)({ a: "a", b: 12 })) // Output: { a: 'a' } (Member1 matched first, so `b` was ignored) // ✅ Define a union where Member2 appears first const Good = Schema.Union(Member2, Member1) console.log(Schema.decodeUnknownSync(Good)({ a: "a", b: 12 })) // Output: { a: 'a', b: 12 } (Member2 matched first, so `b` was included) ``` ### Union of Literals While you can create a union of literals by combining individual literal schemas: **Example** (Using Individual Literal Schemas) ```ts twoslash import { Schema } from "effect" // ┌─── Union<[Schema.Literal<["a"]>, Schema.Literal<["b"]>, Schema.Literal<["c"]>]> // ▼ const schema = Schema.Union( Schema.Literal("a"), Schema.Literal("b"), Schema.Literal("c") ) ``` You can simplify the process by passing multiple literals directly to the `Schema.Literal` constructor: **Example** (Defining a Union of Literals) ```ts twoslash import { Schema } from "effect" // ┌─── Literal<["a", "b", "c"]> // ▼ const schema = Schema.Literal("a", "b", "c") // ┌─── "a" | "b" | "c" // ▼ type Type = typeof schema.Type ``` If you want to set a custom error message for the entire union of literals, you can use the `override: true` option (see [Custom Error Messages](/docs/schema/error-messages/#custom-error-messages) for more details) to specify a unified message. **Example** (Adding a Custom Message to a Union of Literals) ```ts twoslash import { Schema } from "effect" // Schema with individual messages for each literal const individualMessages = Schema.Literal("a", "b", "c") console.log(Schema.decodeUnknownSync(individualMessages)(null)) /* throws: ParseError: "a" | "b" | "c" ├─ Expected "a", actual null ├─ Expected "b", actual null └─ Expected "c", actual null */ // Schema with a unified custom message for all literals const unifiedMessage = Schema.Literal("a", "b", "c").annotations({ message: () => ({ message: "Not a valid code", override: true }) }) console.log(Schema.decodeUnknownSync(unifiedMessage)(null)) /* throws: ParseError: Not a valid code */ ``` ### Nullables The Schema module includes utility functions for defining schemas that allow nullable types, helping to handle values that may be `null`, `undefined`, or both. **Example** (Creating Nullable Schemas) ```ts twoslash import { Schema } from "effect" // Represents a schema for a string or null value Schema.NullOr(Schema.String) // Represents a schema for a string, null, or undefined value Schema.NullishOr(Schema.String) // Represents a schema for a string or undefined value Schema.UndefinedOr(Schema.String) ``` ### Discriminated unions [Discriminated unions](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) in TypeScript are a way of modeling complex data structures that may take on different forms based on a specific set of conditions or properties. They allow you to define a type that represents multiple related shapes, where each shape is uniquely identified by a shared discriminant property. In a discriminated union, each variant of the union has a common property, called the discriminant. The discriminant is a literal type, which means it can only have a finite set of possible values. Based on the value of the discriminant property, TypeScript can infer which variant of the union is currently in use. **Example** (Defining a Discriminated Union in TypeScript) ```ts twoslash type Circle = { readonly kind: "circle" readonly radius: number } type Square = { readonly kind: "square" readonly sideLength: number } type Shape = Circle | Square ``` In the `Schema` module, you can define a discriminated union similarly by specifying a literal field as the discriminant for each type. **Example** (Defining a Discriminated Union Using Schema) ```ts twoslash import { Schema } from "effect" const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) const Square = Schema.Struct({ kind: Schema.Literal("square"), sideLength: Schema.Number }) const Shape = Schema.Union(Circle, Square) ``` In this example, the `Schema.Literal` constructor sets up the `kind` property as the discriminant for both `Circle` and `Square` schemas. The `Shape` schema then represents a union of these two types, allowing TypeScript to infer the specific shape based on the `kind` value. ### Transforming a Simple Union into a Discriminated Union If you start with a simple union and want to transform it into a discriminated union, you can add a special property to each member. This allows TypeScript to automatically infer the correct type based on the value of the discriminant property. **Example** (Initial Simple Union) For example, let's say you've defined a `Shape` union as a combination of `Circle` and `Square` without any special property: ```ts twoslash import { Schema } from "effect" const Circle = Schema.Struct({ radius: Schema.Number }) const Square = Schema.Struct({ sideLength: Schema.Number }) const Shape = Schema.Union(Circle, Square) ``` To make your code more manageable, you may want to transform the simple union into a discriminated union. This way, TypeScript will be able to automatically determine which member of the union you're working with based on the value of a specific property. To achieve this, you can add a special property to each member of the union, which will allow TypeScript to know which type it's dealing with at runtime. Here's how you can [transform](/docs/schema/transformations/#transform) the `Shape` schema into another schema that represents a discriminated union: **Example** (Adding Discriminant Property) ```ts twoslash import { Schema } from "effect" const Circle = Schema.Struct({ radius: Schema.Number }) const Square = Schema.Struct({ sideLength: Schema.Number }) const DiscriminatedShape = Schema.Union( Schema.transform( Circle, // Add a "kind" property with the literal value "circle" to Circle Schema.Struct({ ...Circle.fields, kind: Schema.Literal("circle") }), { strict: true, // Add the discriminant property to Circle decode: (circle) => ({ ...circle, kind: "circle" as const }), // Remove the discriminant property encode: ({ kind: _kind, ...rest }) => rest } ), Schema.transform( Square, // Add a "kind" property with the literal value "square" to Square Schema.Struct({ ...Square.fields, kind: Schema.Literal("square") }), { strict: true, // Add the discriminant property to Square decode: (square) => ({ ...square, kind: "square" as const }), // Remove the discriminant property encode: ({ kind: _kind, ...rest }) => rest } ) ) console.log(Schema.decodeUnknownSync(DiscriminatedShape)({ radius: 10 })) // Output: { radius: 10, kind: 'circle' } console.log( Schema.decodeUnknownSync(DiscriminatedShape)({ sideLength: 10 }) ) // Output: { sideLength: 10, kind: 'square' } ``` The previous solution works perfectly and shows how we can add properties to our schema at will, making it easier to consume the result within our domain model. However, it requires a lot of boilerplate. Fortunately, there is an API called `Schema.attachPropertySignature` designed specifically for this use case, which allows us to achieve the same result with much less effort: **Example** (Using `Schema.attachPropertySignature` for Less Code) ```ts twoslash import { Schema } from "effect" const Circle = Schema.Struct({ radius: Schema.Number }) const Square = Schema.Struct({ sideLength: Schema.Number }) const DiscriminatedShape = Schema.Union( Circle.pipe(Schema.attachPropertySignature("kind", "circle")), Square.pipe(Schema.attachPropertySignature("kind", "square")) ) // decoding console.log(Schema.decodeUnknownSync(DiscriminatedShape)({ radius: 10 })) // Output: { radius: 10, kind: 'circle' } // encoding console.log( Schema.encodeSync(DiscriminatedShape)({ kind: "circle", radius: 10 }) ) // Output: { radius: 10 } ``` ### Exposed Values You can access the individual members of a union schema represented as a tuple: ```ts twoslash import { Schema } from "effect" const schema = Schema.Union(Schema.String, Schema.Number) // Accesses the members of the union const members = schema.members // ┌─── typeof Schema.String // ▼ const firstMember = members[0] // ┌─── typeof Schema.Number // ▼ const secondMember = members[1] ``` ## Tuples The Schema module allows you to define tuples, which are ordered collections of elements that may have different types. You can define tuples with required, optional, or rest elements. ### Required Elements To define a tuple with required elements, you can use the `Schema.Tuple` constructor and simply list the element schemas in order: **Example** (Defining a Tuple with Required Elements) ```ts twoslash import { Schema } from "effect" // Define a tuple with a string and a number as required elements // // ┌─── Tuple<[typeof Schema.String, typeof Schema.Number]> // ▼ const schema = Schema.Tuple(Schema.String, Schema.Number) // ┌─── readonly [string, number] // ▼ type Type = typeof schema.Type ``` ### Append a Required Element You can append additional required elements to an existing tuple by using the spread operator: **Example** (Adding an Element to an Existing Tuple) ```ts twoslash import { Schema } from "effect" const tuple1 = Schema.Tuple(Schema.String, Schema.Number) // Append a boolean to the existing tuple const tuple2 = Schema.Tuple(...tuple1.elements, Schema.Boolean) // ┌─── readonly [string, number, boolean] // ▼ type Type = typeof tuple2.Type ``` ### Optional Elements To define an optional element, use the `Schema.optionalElement` constructor. **Example** (Defining a Tuple with Optional Elements) ```ts twoslash import { Schema } from "effect" // Define a tuple with a required string and an optional number const schema = Schema.Tuple( Schema.String, // required element Schema.optionalElement(Schema.Number) // optional element ) // ┌─── readonly [string, number?] // ▼ type Type = typeof schema.Type ``` ### Rest Element To define a rest element, add it after the list of required or optional elements. The rest element allows the tuple to accept additional elements of a specific type. **Example** (Using a Rest Element) ```ts twoslash import { Schema } from "effect" // Define a tuple with required elements and a rest element of type boolean const schema = Schema.Tuple( [Schema.String, Schema.optionalElement(Schema.Number)], // elements Schema.Boolean // rest element ) // ┌─── readonly [string, number?, ...boolean[]] // ▼ type Type = typeof schema.Type ``` You can also include other elements after the rest: **Example** (Including Additional Elements After a Rest Element) ```ts twoslash import { Schema } from "effect" // Define a tuple with required elements, a rest element, // and an additional element const schema = Schema.Tuple( [Schema.String, Schema.optionalElement(Schema.Number)], // elements Schema.Boolean, // rest element Schema.String // additional element ) // ┌─── readonly [string, number | undefined, ...boolean[], string] // ▼ type Type = typeof schema.Type ``` ### Annotations Annotations are useful for adding metadata to tuple elements, making it easier to describe their purpose or requirements. This is especially helpful for generating documentation or JSON schemas. **Example** (Adding Annotations to Tuple Elements) ```ts twoslash import { JSONSchema, Schema } from "effect" // Define a tuple representing a point with annotations for each coordinate const Point = Schema.Tuple( Schema.element(Schema.Number).annotations({ title: "X", description: "X coordinate" }), Schema.optionalElement(Schema.Number).annotations({ title: "Y", description: "optional Y coordinate" }) ) // Generate a JSON Schema from the tuple console.log(JSONSchema.make(Point)) /* Output: { '$schema': 'http://json-schema.org/draft-07/schema#', type: 'array', minItems: 1, items: [ { type: 'number', description: 'X coordinate', title: 'X' }, { type: 'number', description: 'optional Y coordinate', title: 'Y' } ], additionalItems: false } */ ``` ### Exposed Values You can access the elements and rest elements of a tuple schema using the `elements` and `rest` properties: **Example** (Accessing Elements and Rest Element in a Tuple Schema) ```ts twoslash import { Schema } from "effect" // Define a tuple with required, optional, and rest elements const schema = Schema.Tuple( [Schema.String, Schema.optionalElement(Schema.Number)], // elements Schema.Boolean, // rest element Schema.String // additional element ) // Access the required and optional elements of the tuple // // ┌─── readonly [typeof Schema.String, Schema.Element] // ▼ const tupleElements = schema.elements // Access the rest element of the tuple // // ┌─── readonly [typeof Schema.Boolean, typeof Schema.String] // ▼ const restElement = schema.rest ``` ## Arrays The Schema module allows you to define schemas for arrays, making it easy to validate collections of elements of a specific type. **Example** (Defining an Array Schema) ```ts twoslash import { Schema } from "effect" // Define a schema for an array of numbers // // ┌─── Array$ // ▼ const schema = Schema.Array(Schema.Number) // ┌─── readonly number[] // ▼ type Type = typeof schema.Type ``` ### Mutable Arrays By default, `Schema.Array` generates a type marked as `readonly`. To create a schema for a mutable array, you can use the `Schema.mutable` function, which makes the array type mutable in a **shallow** manner. **Example** (Creating a Mutable Array Schema) ```ts twoslash import { Schema } from "effect" // Define a schema for a mutable array of numbers // // ┌─── mutable> // ▼ const schema = Schema.mutable(Schema.Array(Schema.Number)) // ┌─── number[] // ▼ type Type = typeof schema.Type ``` ### Exposed Values You can access the value type of an array schema using the `value` property: **Example** (Accessing the Value Type of an Array Schema) ```ts twoslash import { Schema } from "effect" const schema = Schema.Array(Schema.Number) // Access the value type of the array schema // // ┌─── typeof Schema.Number // ▼ const value = schema.value ``` ## Non Empty Arrays The Schema module also provides a way to define schemas for non-empty arrays, ensuring that the array always contains at least one element. **Example** (Defining a Non-Empty Array Schema) ```ts twoslash import { Schema } from "effect" // Define a schema for a non-empty array of numbers // // ┌─── NonEmptyArray // ▼ const schema = Schema.NonEmptyArray(Schema.Number) // ┌─── readonly [number, ...number[]] // ▼ type Type = typeof schema.Type ``` ### Exposed Values You can access the value type of a non-empty array schema using the `value` property: **Example** (Accessing the Value Type of a Non-Empty Array Schema) ```ts twoslash import { Schema } from "effect" // Define a schema for a non-empty array of numbers const schema = Schema.NonEmptyArray(Schema.Number) // Access the value type of the non-empty array schema // // ┌─── typeof Schema.Number // ▼ const value = schema.value ``` ## Records The Schema module provides support for defining record types, which are collections of key-value pairs where the key can be a string, symbol, or other types, and the value has a defined schema. ### String Keys You can define a record with string keys and a specified type for the values. **Example** (String Keys with Number Values) ```ts twoslash import { Schema } from "effect" // Define a record schema with string keys and number values // // ┌─── Record$ // ▼ const schema = Schema.Record({ key: Schema.String, value: Schema.Number }) // ┌─── { readonly [x: string]: number; } // ▼ type Type = typeof schema.Type ``` ### Symbol Keys Records can also use symbols as keys. **Example** (Symbol Keys with Number Values) ```ts twoslash import { Schema } from "effect" // Define a record schema with symbol keys and number values const schema = Schema.Record({ key: Schema.SymbolFromSelf, value: Schema.Number }) // ┌─── { readonly [x: symbol]: number; } // ▼ type Type = typeof schema.Type ``` ### Union of Literal Keys Use a union of literals to restrict keys to a specific set of values. **Example** (Union of String Literals as Keys) ```ts twoslash import { Schema } from "effect" // Define a record schema where keys are limited // to specific string literals ("a" or "b") const schema = Schema.Record({ key: Schema.Union(Schema.Literal("a"), Schema.Literal("b")), value: Schema.Number }) // ┌─── { readonly a: number; readonly b: number; } // ▼ type Type = typeof schema.Type ``` ### Template Literal Keys Records can use template literals as keys, allowing for more complex key patterns. **Example** (Template Literal Keys with Number Values) ```ts twoslash import { Schema } from "effect" // Define a record schema with keys that match // the template literal pattern "a${string}" const schema = Schema.Record({ key: Schema.TemplateLiteral(Schema.Literal("a"), Schema.String), value: Schema.Number }) // ┌─── { readonly [x: `a${string}`]: number; } // ▼ type Type = typeof schema.Type ``` ### Refined Keys You can refine the key type with additional constraints. **Example** (Filtering Keys by Minimum Length) ```ts twoslash import { Schema } from "effect" // Define a record schema where keys are strings with a minimum length of 2 const schema = Schema.Record({ key: Schema.String.pipe(Schema.minLength(2)), value: Schema.Number }) // ┌─── { readonly [x: string]: number; } // ▼ type Type = typeof schema.Type ``` Refinements on keys act as filters rather than causing a decoding failure. If a key does not meet the constraints (such as a pattern or minimum length check), it is removed from the decoded output instead of triggering an error. **Example** (Keys That Do Not Meet Constraints Are Removed) ```ts twoslash import { Schema } from "effect" const schema = Schema.Record({ key: Schema.String.pipe(Schema.minLength(2)), value: Schema.Number }) console.log(Schema.decodeUnknownSync(schema)({ a: 1, bb: 2 })) // Output: { bb: 2 } ("a" is removed because it is too short) ``` If you want decoding to fail when a key does not meet the constraints, you can set [`onExcessProperty`](/docs/schema/getting-started/#managing-excess-properties) to `"error"`. **Example** (Forcing an Error on Invalid Keys) ```ts twoslash "onExcessProperty" import { Schema } from "effect" const schema = Schema.Record({ key: Schema.String.pipe(Schema.minLength(2)), value: Schema.Number }) console.log( Schema.decodeUnknownSync(schema, { onExcessProperty: "error" })({ a: 1, bb: 2 }) ) /* throws: ParseError: { readonly [x: minLength(2)]: number } └─ ["a"] └─ is unexpected, expected: minLength(2) */ ``` ### Transforming Keys The `Schema.Record` API does not support transformations on key schemas. Attempting to apply a transformation to keys will result in an `Unsupported key schema` error: **Example** (Attempting to Transform Keys) ```ts twoslash import { Schema } from "effect" const schema = Schema.Record({ key: Schema.Trim, value: Schema.NumberFromString }) /* throws: Error: Unsupported key schema schema (Transformation): Trim */ ``` To modify record keys, you must apply transformations outside of `Schema.Record`. A common approach is to use [Schema.transform](/docs/schema/transformations/#transform) to adjust keys during decoding. **Example** (Trimming Keys While Decoding) ```ts twoslash import { Schema, Record, identity } from "effect" const schema = Schema.transform( // Define the input schema with unprocessed keys Schema.Record({ key: Schema.String, value: Schema.NumberFromString }), // Define the output schema with transformed keys Schema.Record({ key: Schema.Trimmed, value: Schema.Number }), { strict: true, // Trim keys during decoding decode: (record) => Record.mapKeys(record, (key) => key.trim()), encode: identity } ) console.log( Schema.decodeUnknownSync(schema)({ " key1 ": "1", key2: "2" }) ) // Output: { key1: 1, key2: 2 } ``` ### Mutable Records By default, `Schema.Record` generates a type marked as `readonly`. To create a schema for a mutable record, you can use the `Schema.mutable` function, which makes the record type mutable in a **shallow** manner. **Example** (Creating a Mutable Record Schema) ```ts twoslash import { Schema } from "effect" // Create a schema for a mutable record with string keys and number values const schema = Schema.mutable( Schema.Record({ key: Schema.String, value: Schema.Number }) ) // ┌─── { [x: string]: number; } // ▼ type Type = typeof schema.Type ``` ### Exposed Values You can access the `key` and `value` types of a record schema using the `key` and `value` properties: **Example** (Accessing Key and Value Types) ```ts twoslash import { Schema } from "effect" const schema = Schema.Record({ key: Schema.String, value: Schema.Number }) // Accesses the key // // ┌─── typeof Schema.String // ▼ const key = schema.key // Accesses the value // // ┌─── typeof Schema.Number // ▼ const value = schema.value ``` ## Structs ### Property Signatures The `Schema.Struct` constructor defines a schema for an object with specific properties. **Example** (Defining a Struct Schema) This example defines a struct schema for an object with the following properties: - `name`: a string - `age`: a number ```ts twoslash import { Schema } from "effect" // ┌─── Schema.Struct<{ // │ name: typeof Schema.String; // │ age: typeof Schema.Number; // │ }> // ▼ const schema = Schema.Struct({ name: Schema.String, age: Schema.Number }) // The inferred TypeScript type from the schema // // ┌─── { // │ readonly name: string; // │ readonly age: number; // │ } // ▼ type Type = typeof schema.Type ``` ### Index Signatures The `Schema.Struct` constructor can optionally accept a list of key/value pairs representing index signatures, allowing you to define additional dynamic properties. ```ts showLineNumbers=false declare const Struct: (props, ...indexSignatures) => Struct<...> ``` **Example** (Adding an Index Signature) ```ts twoslash import { Schema } from "effect" // Define a struct with a specific property "a" // and an index signature allowing additional properties const schema = Schema.Struct( // Defined properties { a: Schema.Number }, // Index signature: allows additional string keys with number values { key: Schema.String, value: Schema.Number } ) // The inferred TypeScript type: // // ┌─── { // │ readonly [x: string]: number; // │ readonly a: number; // │ } // ▼ type Type = typeof schema.Type ``` **Example** (Using `Schema.Record`) You can achieve the same result using `Schema.Record`: ```ts twoslash import { Schema } from "effect" // Define a struct with a fixed property "a" // and a dynamic index signature using Schema.Record const schema = Schema.Struct( { a: Schema.Number }, Schema.Record({ key: Schema.String, value: Schema.Number }) ) // The inferred TypeScript type: // // ┌─── { // │ readonly [x: string]: number; // │ readonly a: number; // │ } // ▼ type Type = typeof schema.Type ``` ### Multiple Index Signatures You can define **one** index signature per key type (`string` or `symbol`). Defining multiple index signatures of the same type is not allowed. **Example** (Valid Multiple Index Signatures) ```ts twoslash import { Schema } from "effect" // Define a struct with a fixed property "a" // and valid index signatures for both strings and symbols const schema = Schema.Struct( { a: Schema.Number }, // String index signature { key: Schema.String, value: Schema.Number }, // Symbol index signature { key: Schema.SymbolFromSelf, value: Schema.Number } ) // The inferred TypeScript type: // // ┌─── { // │ readonly [x: string]: number; // │ readonly [x: symbol]: number; // │ readonly a: number; // │ } // ▼ type Type = typeof schema.Type ``` Defining multiple index signatures of the same key type (`string` or `symbol`) will cause an error. **Example** (Invalid Multiple Index Signatures) ```ts twoslash import { Schema } from "effect" Schema.Struct( { a: Schema.Number }, // Attempting to define multiple string index signatures { key: Schema.String, value: Schema.Number }, { key: Schema.String, value: Schema.Boolean } ) /* throws: Error: Duplicate index signature details: string index signature */ ``` ### Conflicting Index Signatures When defining schemas with index signatures, conflicts can arise if a fixed property has a different type than the values allowed by the index signature. This can lead to unexpected TypeScript behavior. **Example** (Conflicting Index Signature) ```ts twoslash import { Schema } from "effect" // Attempting to define a struct with a conflicting index signature // - The fixed property "a" is a number // - The index signature requires all values to be strings const schema = Schema.Struct( { a: Schema.String }, { key: Schema.String, value: Schema.Number } ) // ❌ Incorrect TypeScript type: // // ┌─── { // │ readonly [x: string]: number; // │ readonly a: string; // │ } // ▼ type Type = typeof schema.Type ``` The TypeScript compiler flags this as an error when defining the type manually: ```ts twoslash // @errors: 2411 // This type is invalid because the index signature // conflicts with the fixed property `a` type Test = { readonly a: string readonly [x: string]: number } ``` This happens because TypeScript does not allow an index signature to contradict a fixed property. #### Workaround for Conflicting Index Signatures When working with schemas, a conflict can occur if a fixed property has a different type than the values allowed by an index signature. This situation often arises when dealing with external APIs that do not follow strict TypeScript conventions. To prevent conflicts, you can separate the fixed properties from the indexed properties and handle them as distinct parts of the schema. **Example** (Extracting Fixed and Indexed Properties) Consider an object where: - `"a"` is a fixed property of type `string`. - All other keys store numbers, which conflict with `"a"`. ```ts twoslash // @errors: 2411 // This type is invalid because the index signature // conflicts with the fixed property `a` type Test = { a: string [x: string]: number } ``` To avoid this issue, we can separate the properties into two distinct types: ```ts showLineNumbers=false // Fixed properties schema type FixedProperties = { readonly a: string } // Index signature properties schema type IndexSignatureProperties = { readonly [x: string]: number } // The final output groups both properties in a tuple type OutputData = readonly [FixedProperties, IndexSignatureProperties] ``` By using [Schema.transform](/docs/schema/transformations/#transform) and [Schema.compose](/docs/schema/transformations/#composition), you can preprocess the input data before validation. This approach ensures that fixed properties and index signature properties are treated independently. ```ts twoslash import { Schema } from "effect" // Define a schema for the fixed property "a" const FixedProperties = Schema.Struct({ a: Schema.String }) // Define a schema for index signature properties const IndexSignatureProperties = Schema.Record({ // Exclude keys that are already present in FixedProperties key: Schema.String.pipe( Schema.filter( (key) => !Object.keys(FixedProperties.fields).includes(key) ) ), value: Schema.Number }) // Create a schema that duplicates an object into two parts const Duplicate = Schema.transform( Schema.Object, Schema.Tuple(Schema.Object, Schema.Object), { strict: true, // Create a tuple containing the input twice decode: (a) => [a, a] as const, // Merge both parts back when encoding encode: ([a, b]) => ({ ...a, ...b }) } ) // ┌─── Schema // ▼ const Result = Schema.compose( Duplicate, Schema.Tuple(FixedProperties, IndexSignatureProperties).annotations({ parseOptions: { onExcessProperty: "ignore" } }) ) // Decoding: Separates fixed and indexed properties console.log(Schema.decodeUnknownSync(Result)({ a: "a", b: 1, c: 2 })) // Output: [ { a: 'a' }, { b: 1, c: 2 } ] // Encoding: Combines them back into an object console.log(Schema.encodeSync(Result)([{ a: "a" }, { b: 1, c: 2 }])) // Output: { a: 'a', b: 1, c: 2 } ``` ### Exposed Values You can access the fields and records of a struct schema using the `fields` and `records` properties: **Example** (Accessing Fields and Records) ```ts twoslash import { Schema } from "effect" const schema = Schema.Struct( { a: Schema.Number }, Schema.Record({ key: Schema.String, value: Schema.Number }) ) // Accesses the fields // // ┌─── { readonly a: typeof Schema.Number; } // ▼ const fields = schema.fields // Accesses the records // // ┌─── readonly [Schema.Record$] // ▼ const records = schema.records ``` ### Mutable Structs By default, `Schema.Struct` generates a type with properties marked as `readonly`. To create a mutable version of the struct, use the `Schema.mutable` function, which makes the properties mutable in a **shallow** manner. **Example** (Creating a Mutable Struct Schema) ```ts twoslash import { Schema } from "effect" const schema = Schema.mutable( Schema.Struct({ a: Schema.String, b: Schema.Number }) ) // ┌─── { a: string; b: number; } // ▼ type Type = typeof schema.Type ``` ## Tagged Structs In TypeScript tags help to enhance type discrimination and pattern matching by providing a simple yet powerful way to define and recognize different data types. ### What is a Tag? A tag is a literal value added to data structures, commonly used in structs, to distinguish between various object types or variants within tagged unions. This literal acts as a discriminator, making it easier to handle and process different types of data correctly and efficiently. ### Using the tag Constructor The `Schema.tag` constructor is specifically designed to create a property signature that holds a specific literal value, serving as the discriminator for object types. **Example** (Defining a Tagged Struct) ```ts twoslash import { Schema } from "effect" const User = Schema.Struct({ _tag: Schema.tag("User"), name: Schema.String, age: Schema.Number }) // ┌─── { readonly _tag: "User"; readonly name: string; readonly age: number; } // ▼ type Type = typeof User.Type console.log(User.make({ name: "John", age: 44 })) /* Output: { _tag: 'User', name: 'John', age: 44 } */ ``` In the example above, `Schema.tag("User")` attaches a `_tag` property to the `User` struct schema, effectively labeling objects of this struct type as "User". This label is automatically applied when using the `make` method to create new instances, simplifying object creation and ensuring consistent tagging. ### Simplifying Tagged Structs with TaggedStruct The `Schema.TaggedStruct` constructor streamlines the process of creating tagged structs by directly integrating the tag into the struct definition. This method provides a clearer and more declarative approach to building data structures with embedded discriminators. **Example** (Using `TaggedStruct` for a Simplified Tagged Struct) ```ts twoslash import { Schema } from "effect" const User = Schema.TaggedStruct("User", { name: Schema.String, age: Schema.Number }) // `_tag` is automatically applied when constructing an instance console.log(User.make({ name: "John", age: 44 })) // Output: { _tag: 'User', name: 'John', age: 44 } // `_tag` is required when decoding from an unknown source console.log(Schema.decodeUnknownSync(User)({ name: "John", age: 44 })) /* throws: ParseError: { readonly _tag: "User"; readonly name: string; readonly age: number } └─ ["_tag"] └─ is missing */ ``` In this example: - The `_tag` property is optional when constructing an instance with `make`, allowing the schema to automatically apply it. - When decoding unknown data, `_tag` is required to ensure correct type identification. This distinction between instance construction and decoding is useful for preserving the tag’s role as a type discriminator while simplifying instance creation. If you need `_tag` to be applied automatically during decoding as well, you can create a customized version of `Schema.TaggedStruct`: **Example** (Custom `TaggedStruct` with `_tag` Applied during Decoding) ```ts twoslash import type { SchemaAST } from "effect" import { Schema } from "effect" const TaggedStruct = < Tag extends SchemaAST.LiteralValue, Fields extends Schema.Struct.Fields >( tag: Tag, fields: Fields ) => Schema.Struct({ _tag: Schema.Literal(tag).pipe( Schema.optional, Schema.withDefaults({ constructor: () => tag, // Apply _tag during instance construction decoding: () => tag // Apply _tag during decoding }) ), ...fields }) const User = TaggedStruct("User", { name: Schema.String, age: Schema.Number }) console.log(User.make({ name: "John", age: 44 })) // Output: { _tag: 'User', name: 'John', age: 44 } console.log(Schema.decodeUnknownSync(User)({ name: "John", age: 44 })) // Output: { _tag: 'User', name: 'John', age: 44 } ``` ### Multiple Tags While a primary tag is often sufficient, TypeScript allows you to define multiple tags for more complex data structuring needs. Here's an example demonstrating the use of multiple tags within a single struct: **Example** (Adding Multiple Tags to a Struct) This example defines a product schema with a primary tag (`"Product"`) and an additional category tag (`"Electronics"`), adding further specificity to the data structure. ```ts twoslash import { Schema } from "effect" const Product = Schema.TaggedStruct("Product", { category: Schema.tag("Electronics"), name: Schema.String, price: Schema.Number }) // `_tag` and `category` are optional when creating an instance console.log(Product.make({ name: "Smartphone", price: 999 })) /* Output: { _tag: 'Product', category: 'Electronics', name: 'Smartphone', price: 999 } */ ``` ## instanceOf When you need to define a schema for your custom data type defined through a `class`, the most convenient and fast way is to use the `Schema.instanceOf` constructor. **Example** (Defining a Schema with `instanceOf`) ```ts twoslash import { Schema } from "effect" // Define a custom class class MyData { constructor(readonly name: string) {} } // Create a schema for the class const MyDataSchema = Schema.instanceOf(MyData) // ┌─── MyData // ▼ type Type = typeof MyDataSchema.Type console.log(Schema.decodeUnknownSync(MyDataSchema)(new MyData("name"))) // Output: MyData { name: 'name' } console.log(Schema.decodeUnknownSync(MyDataSchema)({ name: "name" })) /* throws: ParseError: Expected MyData, actual {"name":"name"} */ ``` The `Schema.instanceOf` constructor is just a lightweight wrapper of the [Schema.declare](/docs/schema/advanced-usage/#declaring-new-data-types) API, which is the primitive in `effect/Schema` for declaring new custom data types. ### Private Constructors Note that `Schema.instanceOf` can only be used for classes that expose a **public constructor**. If you try to use it with classes that, for some reason, have marked the constructor as `private`, you'll receive a TypeScript error: **Example** (Error With Private Constructors) ```ts twoslash import { Schema } from "effect" class MyData { static make = (name: string) => new MyData(name) private constructor(readonly name: string) {} } // @ts-expect-error const MyDataSchema = Schema.instanceOf(MyData) /* Argument of type 'typeof MyData' is not assignable to parameter of type 'abstract new (...args: any) => any'. Cannot assign a 'private' constructor type to a 'public' constructor type.ts(2345) */ ``` In such cases, you cannot use `Schema.instanceOf`, and you must rely on [Schema.declare](/docs/schema/advanced-usage/#declaring-new-data-types) like this: **Example** (Using `Schema.declare` With Private Constructors) ```ts twoslash import { Schema } from "effect" class MyData { static make = (name: string) => new MyData(name) private constructor(readonly name: string) {} } const MyDataSchema = Schema.declare( (input: unknown): input is MyData => input instanceof MyData ).annotations({ identifier: "MyData" }) console.log(Schema.decodeUnknownSync(MyDataSchema)(MyData.make("name"))) // Output: MyData { name: 'name' } console.log(Schema.decodeUnknownSync(MyDataSchema)({ name: "name" })) /* throws: ParseError: Expected MyData, actual {"name":"name"} */ ``` ### Validating Fields of the Instance To validate the fields of a class instance, you can use a [filter](/docs/schema/filters/). This approach combines instance validation with additional checks on the instance's fields. **Example** (Adding Field Validation to an Instance Schema) ```ts twoslash import { Either, ParseResult, Schema } from "effect" class MyData { constructor(readonly name: string) {} } const MyDataFields = Schema.Struct({ name: Schema.NonEmptyString }) // Define a schema for the class instance with additional field validation const MyDataSchema = Schema.instanceOf(MyData).pipe( Schema.filter((a, options) => // Validate the fields of the instance ParseResult.validateEither(MyDataFields)(a, options).pipe( // Invert success and failure for filtering Either.flip, // Return undefined if validation succeeds, or an error if it fails Either.getOrUndefined ) ) ) // Example: Valid instance console.log(Schema.validateSync(MyDataSchema)(new MyData("John"))) // Output: MyData { name: 'John' } // Example: Invalid instance (empty name) console.log(Schema.validateSync(MyDataSchema)(new MyData(""))) /* throws: ParseError: { MyData | filter } └─ Predicate refinement failure └─ { readonly name: NonEmptyString } └─ ["name"] └─ NonEmptyString └─ Predicate refinement failure └─ Expected a non empty string, actual "" */ ``` ## Picking The `pick` static function available on each struct schema can be used to create a new `Struct` by selecting specific properties from an existing `Struct`. **Example** (Picking Properties from a Struct) ```ts twoslash import { Schema } from "effect" // Define a struct schema with properties "a", "b", and "c" const MyStruct = Schema.Struct({ a: Schema.String, b: Schema.Number, c: Schema.Boolean }) // Create a new schema that picks properties "a" and "c" // // ┌─── Struct<{ // | a: typeof Schema.String; // | c: typeof Schema.Boolean; // | }> // ▼ const PickedSchema = MyStruct.pick("a", "c") ``` The `Schema.pick` function can be applied more broadly beyond just `Struct` types, such as with unions of schemas. However it returns a generic `SchemaClass`. **Example** (Picking Properties from a Union) ```ts twoslash import { Schema } from "effect" // Define a union of two struct schemas const MyUnion = Schema.Union( Schema.Struct({ a: Schema.String, b: Schema.String, c: Schema.String }), Schema.Struct({ a: Schema.Number, b: Schema.Number, d: Schema.Number }) ) // Create a new schema that picks properties "a" and "b" // // ┌─── SchemaClass<{ // | readonly a: string | number; // | readonly b: string | number; // | }> // ▼ const PickedSchema = MyUnion.pipe(Schema.pick("a", "b")) ``` ## Omitting The `omit` static function available in each struct schema can be used to create a new `Struct` by excluding particular properties from an existing `Struct`. **Example** (Omitting Properties from a Struct) ```ts twoslash import { Schema } from "effect" // Define a struct schema with properties "a", "b", and "c" const MyStruct = Schema.Struct({ a: Schema.String, b: Schema.Number, c: Schema.Boolean }) // Create a new schema that omits property "b" // // ┌─── Schema.Struct<{ // | a: typeof Schema.String; // | c: typeof Schema.Boolean; // | }> // ▼ const PickedSchema = MyStruct.omit("b") ``` The `Schema.omit` function can be applied more broadly beyond just `Struct` types, such as with unions of schemas. However it returns a generic `Schema`. **Example** (Omitting Properties from a Union) ```ts twoslash import { Schema } from "effect" // Define a union of two struct schemas const MyUnion = Schema.Union( Schema.Struct({ a: Schema.String, b: Schema.String, c: Schema.String }), Schema.Struct({ a: Schema.Number, b: Schema.Number, d: Schema.Number }) ) // Create a new schema that omits property "b" // // ┌─── SchemaClass<{ // | readonly a: string | number; // | }> // ▼ const PickedSchema = MyUnion.pipe(Schema.omit("b")) ``` ## partial The `Schema.partial` function makes all properties within a schema optional. **Example** (Making All Properties Optional) ```ts twoslash import { Schema } from "effect" // Create a schema with an optional property "a" const schema = Schema.partial(Schema.Struct({ a: Schema.String })) // ┌─── { readonly a?: string | undefined; } // ▼ type Type = typeof schema.Type ``` By default, the `Schema.partial` operation adds `undefined` to the type of each property. If you want to avoid this, you can use `Schema.partialWith` and pass `{ exact: true }` as an argument. **Example** (Defining an Exact Partial Schema) ```ts twoslash import { Schema } from "effect" // Create a schema with an optional property "a" without allowing undefined const schema = Schema.partialWith( Schema.Struct({ a: Schema.String }), { exact: true } ) // ┌─── { readonly a?: string; } // ▼ type Type = typeof schema.Type ``` ## required The `Schema.required` function ensures that all properties in a schema are mandatory. **Example** (Making All Properties Required) ```ts twoslash import { Schema } from "effect" // Create a schema and make all properties required const schema = Schema.required( Schema.Struct({ a: Schema.optionalWith(Schema.String, { exact: true }), b: Schema.optionalWith(Schema.Number, { exact: true }) }) ) // ┌─── { readonly a: string; readonly b: number; } // ▼ type Type = typeof schema.Type ``` In this example, both `a` and `b` are made required, even though they were initially defined as optional. ## keyof The `Schema.keyof` operation creates a schema that represents the keys of a given object schema. **Example** (Extracting Keys from an Object Schema) ```ts twoslash import { Schema } from "effect" const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }) const keys = Schema.keyof(schema) // ┌─── "a" | "b" // ▼ type Type = typeof keys.Type ``` # [Class APIs](https://effect.website/docs/schema/classes/) ## Overview import { Aside } from "@astrojs/starlight/components" When working with schemas, you have a choice beyond the [Schema.Struct](/docs/schema/basic-usage/#structs) constructor. You can leverage the power of classes through the `Schema.Class` utility, which comes with its own set of advantages tailored to common use cases: Classes offer several features that simplify the schema creation process: - **All-in-One Definition**: With classes, you can define both a schema and an opaque type simultaneously. - **Shared Functionality**: You can incorporate shared functionality using class methods or getters. - **Value Hashing and Equality**: Utilize the built-in capability for checking value equality and applying hashing (thanks to `Class` implementing [Data.Class](/docs/data-types/data/#class)). ## Definition To define a class using `Schema.Class`, you need to specify: - The **type** of the class being created. - A unique **identifier** for the class. - The desired **fields**. **Example** (Defining a Schema Class) ```ts twoslash import { Schema } from "effect" class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.NonEmptyString }) {} ``` In this example, `Person` is both a schema and a TypeScript class. Instances of `Person` are created using the defined schema, ensuring compliance with the specified fields. **Example** (Creating Instances) ```ts twoslash import { Schema } from "effect" class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.NonEmptyString }) {} console.log(new Person({ id: 1, name: "John" })) /* Output: Person { id: 1, name: 'John' } */ // Using the factory function console.log(Person.make({ id: 1, name: "John" })) /* Output: Person { id: 1, name: 'John' } */ ``` ### Class Schemas are Transformations Class schemas [transform](/docs/schema/transformations/) a struct schema into a [declaration](/docs/schema/advanced-usage/#declaring-new-data-types) schema that represents a class type. - When decoding, a plain object is converted into an instance of the class. - When encoding, a class instance is converted back into a plain object. **Example** (Decoding and Encoding a Class) ```ts twoslash import { Schema } from "effect" class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.NonEmptyString }) {} const person = Person.make({ id: 1, name: "John" }) // Decode from a plain object into a class instance const decoded = Schema.decodeUnknownSync(Person)({ id: 1, name: "John" }) console.log(decoded) // Output: Person { id: 1, name: 'John' } // Encode a class instance back into a plain object const encoded = Schema.encodeUnknownSync(Person)(person) console.log(encoded) // Output: { id: 1, name: 'John' } ``` ### Defining Classes Without Fields When your schema does not require any fields, you can define a class with an empty object. **Example** (Defining and Using a Class Without Arguments) ```ts twoslash import { Schema } from "effect" // Define a class with no fields class NoArgs extends Schema.Class("NoArgs")({}) {} // Create an instance using the default constructor const noargs1 = new NoArgs() // Alternatively, create an instance by explicitly passing an empty object const noargs2 = new NoArgs({}) ``` ### Defining Classes With Filters Filters allow you to validate input when decoding, encoding, or creating an instance. Instead of specifying raw fields, you can pass a `Schema.Struct` with a filter applied. **Example** (Applying a Filter to a Schema Class) ```ts twoslash import { Schema } from "effect" class WithFilter extends Schema.Class("WithFilter")( Schema.Struct({ a: Schema.NumberFromString, b: Schema.NumberFromString }).pipe( Schema.filter(({ a, b }) => a >= b || "a must be greater than b") ) ) {} // Constructor console.log(new WithFilter({ a: 1, b: 2 })) /* throws: ParseError: WithFilter (Constructor) └─ Predicate refinement failure └─ a must be greater than b */ // Decoding console.log(Schema.decodeUnknownSync(WithFilter)({ a: "1", b: "2" })) /* throws: ParseError: (WithFilter (Encoded side) <-> WithFilter) └─ Encoded side transformation failure └─ WithFilter (Encoded side) └─ Predicate refinement failure └─ a must be greater than b */ ``` ## Validating Properties via Class Constructors When you define a class using `Schema.Class`, the constructor automatically checks that the provided properties adhere to the schema's rules. ### Defining and Instantiating a Valid Class Instance The constructor ensures that each property, like `id` and `name`, adheres to the schema. For instance, `id` must be a number, and `name` must be a non-empty string. **Example** (Creating a Valid Instance) ```ts twoslash import { Schema } from "effect" class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.NonEmptyString }) {} // Create an instance with valid properties const john = new Person({ id: 1, name: "John" }) ``` ### Handling Invalid Properties If invalid properties are provided during instantiation, the constructor throws an error, explaining why the validation failed. **Example** (Creating an Instance with Invalid Properties) ```ts twoslash import { Schema } from "effect" class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.NonEmptyString }) {} // Attempt to create an instance with an invalid `name` new Person({ id: 1, name: "" }) /* throws: ParseError: Person (Constructor) └─ ["name"] └─ NonEmptyString └─ Predicate refinement failure └─ Expected NonEmptyString, actual "" */ ``` The error clearly specifies that the `name` field failed to meet the `NonEmptyString` requirement. ### Bypassing Validation In some scenarios, you might want to bypass the validation logic. While not generally recommended, the library provides an option to do so. **Example** (Bypassing Validation) ```ts twoslash import { Schema } from "effect" class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.NonEmptyString }) {} // Bypass validation during instantiation const john = new Person({ id: 1, name: "" }, true) // Or use the `disableValidation` option explicitly new Person({ id: 1, name: "" }, { disableValidation: true }) ``` ## Automatic Hashing and Equality in Classes Instances of classes created with `Schema.Class` support the [Equal](/docs/trait/equal/) trait through their integration with [Data.Class](/docs/data-types/data/#class). This enables straightforward value comparisons, even across different instances. ### Basic Equality Check Two class instances are considered equal if their properties have identical values. **Example** (Comparing Instances with Equal Properties) ```ts twoslash import { Schema } from "effect" import { Equal } from "effect" class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.NonEmptyString }) {} const john1 = new Person({ id: 1, name: "John" }) const john2 = new Person({ id: 1, name: "John" }) // Compare instances console.log(Equal.equals(john1, john2)) // Output: true ``` ### Nested or Complex Properties The `Equal` trait performs comparisons at the first level. If a property is a more complex structure, such as an array, instances may not be considered equal, even if the arrays themselves have identical values. **Example** (Shallow Equality for Arrays) ```ts twoslash import { Schema } from "effect" import { Equal } from "effect" class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.NonEmptyString, hobbies: Schema.Array(Schema.String) // Standard array schema }) {} const john1 = new Person({ id: 1, name: "John", hobbies: ["reading", "coding"] }) const john2 = new Person({ id: 1, name: "John", hobbies: ["reading", "coding"] }) // Equality fails because `hobbies` are not deeply compared console.log(Equal.equals(john1, john2)) // Output: false ``` To achieve deep equality for nested structures like arrays, use `Schema.Data` in combination with `Data.array`. This enables the library to compare each element of the array rather than treating it as a single entity. **Example** (Using `Schema.Data` for Deep Equality) ```ts twoslash import { Schema } from "effect" import { Data, Equal } from "effect" class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.NonEmptyString, hobbies: Schema.Data(Schema.Array(Schema.String)) // Enable deep equality }) {} const john1 = new Person({ id: 1, name: "John", hobbies: Data.array(["reading", "coding"]) }) const john2 = new Person({ id: 1, name: "John", hobbies: Data.array(["reading", "coding"]) }) // Equality succeeds because `hobbies` are deeply compared console.log(Equal.equals(john1, john2)) // Output: true ``` ## Extending Classes with Custom Logic Schema classes provide the flexibility to include custom getters and methods, allowing you to extend their functionality beyond the defined fields. ### Adding Custom Getters A getter can be used to derive computed values from the fields of the class. For example, a `Person` class can include a getter to return the `name` property in uppercase. **Example** (Adding a Getter for Uppercase Name) ```ts twoslash import { Schema } from "effect" class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.NonEmptyString }) { // Custom getter to return the name in uppercase get upperName() { return this.name.toUpperCase() } } const john = new Person({ id: 1, name: "John" }) // Use the custom getter console.log(john.upperName) // Output: "JOHN" ``` ### Adding Custom Methods In addition to getters, you can define methods to encapsulate more complex logic or operations involving the class's fields. **Example** (Adding a Method) ```ts twoslash import { Schema } from "effect" class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.NonEmptyString }) { // Custom method to return a greeting greet() { return `Hello, my name is ${this.name}.` } } const john = new Person({ id: 1, name: "John" }) // Use the custom method console.log(john.greet()) // Output: "Hello, my name is John." ``` ## Leveraging Classes as Schema Definitions When you define a class with `Schema.Class`, it serves both as a schema and as a class. This dual functionality allows the class to be used wherever a schema is required. **Example** (Using a Class in an Array Schema) ```ts twoslash import { Schema } from "effect" class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.NonEmptyString }) {} // Use the Person class in an array schema const Persons = Schema.Array(Person) // ┌─── readonly Person[] // ▼ type Type = typeof Persons.Type ``` ### Exposed Values The class also includes a `fields` static property, which outlines the fields defined during the class creation. **Example** (Accessing the `fields` Property) ```ts twoslash import { Schema } from "effect" class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.NonEmptyString }) {} // ┌─── { // | readonly id: typeof Schema.Number; // | readonly name: typeof Schema.NonEmptyString; // | } // ▼ Person.fields ``` ## Adding Annotations Defining a class with `Schema.Class` is similar to creating a [transformation](/docs/schema/transformations/) schema that converts a struct schema into a [declaration](/docs/schema/advanced-usage/#declaring-new-data-types) schema representing the class type. For example, consider the following class definition: ```ts twoslash import { Schema } from "effect" class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.NonEmptyString }) {} ``` Under the hood, this definition creates a transformation schema that maps: ```ts showLineNumbers=false Schema.Struct({ id: Schema.Number, name: Schema.NonEmptyString }) ``` to a schema representing the `Person` class: ```ts showLineNumbers=false Schema.declare((input) => input instanceof Person) ``` So, defining a schema with `Schema.Class` involves three schemas: - The "from" schema (the struct) - The "to" schema (the class) - The "transformation" schema (struct -> class) You can annotate each of these schemas by passing a tuple as the second argument to the `Schema.Class` API. **Example** (Annotating Different Parts of the Class Schema) ```ts twoslash import { Schema, SchemaAST } from "effect" class Person extends Schema.Class("Person")( { id: Schema.Number, name: Schema.NonEmptyString }, [ // Annotations for the "to" schema { description: `"to" description` }, // Annotations for the "transformation schema { description: `"transformation" description` }, // Annotations for the "from" schema { description: `"from" description` } ] ) {} console.log(SchemaAST.getDescriptionAnnotation(Person.ast.to)) // Output: { _id: 'Option', _tag: 'Some', value: '"to" description' } console.log(SchemaAST.getDescriptionAnnotation(Person.ast)) // Output: { _id: 'Option', _tag: 'Some', value: '"transformation" description' } console.log(SchemaAST.getDescriptionAnnotation(Person.ast.from)) // Output: { _id: 'Option', _tag: 'Some', value: '"from" description' } ``` If you do not want to annotate all three schemas, you can pass `undefined` for the ones you wish to skip. **Example** (Skipping Annotations) ```ts twoslash import { Schema, SchemaAST } from "effect" class Person extends Schema.Class("Person")( { id: Schema.Number, name: Schema.NonEmptyString }, [ // No annotations for the "to" schema undefined, // Annotations for the "transformation schema { description: `"transformation" description` } ] ) {} console.log(SchemaAST.getDescriptionAnnotation(Person.ast.to)) // Output: { _id: 'Option', _tag: 'None' } console.log(SchemaAST.getDescriptionAnnotation(Person.ast)) // Output: { _id: 'Option', _tag: 'Some', value: '"transformation" description' } console.log(SchemaAST.getDescriptionAnnotation(Person.ast.from)) // Output: { _id: 'Option', _tag: 'None' } ``` By default, the unique identifier used to define the class is also applied as the default `identifier` annotation for the Class Schema. **Example** (Default Identifier Annotation) ```ts twoslash import { Schema, SchemaAST } from "effect" // Used as default identifier annotation ────┐ // | // ▼ class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.NonEmptyString }) {} console.log(SchemaAST.getIdentifierAnnotation(Person.ast.to)) // Output: { _id: 'Option', _tag: 'Some', value: 'Person' } ``` ## Recursive Schemas The `Schema.suspend` combinator is useful when you need to define a schema that depends on itself, like in the case of recursive data structures. In this example, the `Category` schema depends on itself because it has a field `subcategories` that is an array of `Category` objects. **Example** (Self-Referencing Schema) ```ts twoslash import { Schema } from "effect" // Define a Category schema with a recursive subcategories field class Category extends Schema.Class("Category")({ name: Schema.String, subcategories: Schema.Array( Schema.suspend((): Schema.Schema => Category) ) }) {} ``` **Example** (Missing Type Annotation Error) ```ts twoslash import { Schema } from "effect" // @ts-expect-error class Category extends Schema.Class("Category")({ name: Schema.String, // @ts-expect-error: TypeScript cannot infer the recursive type subcategories: Schema.Array(Schema.suspend(() => Category)) }) {} /* 'Category' is referenced directly or indirectly in its own base expression.ts(2506) */ ``` ### Mutually Recursive Schemas Sometimes, schemas depend on each other in a mutually recursive way. For instance, an arithmetic expression tree might include `Expression` nodes that can either be numbers or `Operation` nodes, which in turn reference `Expression` nodes. **Example** (Arithmetic Expression Tree) ```ts twoslash import { Schema } from "effect" class Expression extends Schema.Class("Expression")({ type: Schema.Literal("expression"), value: Schema.Union( Schema.Number, Schema.suspend((): Schema.Schema => Operation) ) }) {} class Operation extends Schema.Class("Operation")({ type: Schema.Literal("operation"), operator: Schema.Literal("+", "-"), left: Expression, right: Expression }) {} ``` ### Recursive Types with Different Encoded and Type Defining recursive schemas where the `Encoded` type differs from the `Type` type introduces additional complexity. For instance, if a schema includes fields that transform data (e.g., `NumberFromString`), the `Encoded` and `Type` types may not align. In such cases, we need to define an interface for the `Encoded` type. Let's consider an example: suppose we want to add an `id` field to the `Category` schema, where the schema for `id` is `NumberFromString`. It's important to note that `NumberFromString` is a schema that transforms a string into a number, so the `Type` and `Encoded` types of `NumberFromString` differ, being `number` and `string` respectively. When we add this field to the `Category` schema, TypeScript raises an error: ```ts twoslash import { Schema } from "effect" class Category extends Schema.Class("Category")({ id: Schema.NumberFromString, name: Schema.String, subcategories: Schema.Array( // @ts-expect-error Schema.suspend((): Schema.Schema => Category) ) }) {} /* Type 'typeof Category' is not assignable to type 'Schema'. The types of 'Encoded.id' are incompatible between these types. Type 'string' is not assignable to type 'number'.ts(2322) */ ``` This error occurs because the explicit annotation `S.suspend((): S.Schema => Category` is no longer sufficient and needs to be adjusted by explicitly adding the `Encoded` type: **Example** (Adjusting the Schema with Explicit `Encoded` Type) ```ts twoslash import { Schema } from "effect" interface CategoryEncoded { readonly id: string readonly name: string readonly subcategories: ReadonlyArray } class Category extends Schema.Class("Category")({ id: Schema.NumberFromString, name: Schema.String, subcategories: Schema.Array( Schema.suspend( (): Schema.Schema => Category ) ) }) {} ``` As we've observed, it's necessary to define an interface for the `Encoded` of the schema to enable recursive schema definition, which can complicate things and be quite tedious. One pattern to mitigate this is to **separate the field responsible for recursion** from all other fields. **Example** (Separating Recursive Field) ```ts twoslash import { Schema } from "effect" const fields = { id: Schema.NumberFromString, name: Schema.String // ...possibly other fields } interface CategoryEncoded extends Schema.Struct.Encoded { // Define `subcategories` using recursion readonly subcategories: ReadonlyArray } class Category extends Schema.Class("Category")({ ...fields, // Include the fields subcategories: Schema.Array( // Define `subcategories` using recursion Schema.suspend( (): Schema.Schema => Category ) ) }) {} ``` ## Tagged Class variants You can also create classes that extend [TaggedClass](/docs/data-types/data/#taggedclass) and [TaggedError](/docs/data-types/data/#taggederror) from the `effect/Data` module. **Example** (Creating Tagged Classes and Errors) ```ts twoslash import { Schema } from "effect" // Define a tagged class with a "name" field class TaggedPerson extends Schema.TaggedClass()( "TaggedPerson", { name: Schema.String } ) {} // Define a tagged error with a "status" field class HttpError extends Schema.TaggedError()("HttpError", { status: Schema.Number }) {} const joe = new TaggedPerson({ name: "Joe" }) console.log(joe._tag) // Output: "TaggedPerson" const error = new HttpError({ status: 404 }) console.log(error._tag) // Output: "HttpError" console.log(error.stack) // access the stack trace ``` ## Extending existing Classes The `extend` static utility allows you to enhance an existing schema class by adding **additional** fields and functionality. This approach helps in building on top of existing schemas without redefining them from scratch. **Example** (Extending a Schema Class) ```ts twoslash import { Schema } from "effect" // Define the base class class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.NonEmptyString }) { // A custom getter that converts the name to uppercase get upperName() { return this.name.toUpperCase() } } // Extend the base class to include an "age" field class PersonWithAge extends Person.extend("PersonWithAge")( { age: Schema.Number } ) { // A custom getter to check if the person is an adult get isAdult() { return this.age >= 18 } } // Usage const john = new PersonWithAge({ id: 1, name: "John", age: 25 }) console.log(john.upperName) // Output: "JOHN" console.log(john.isAdult) // Output: true ``` Note that you can only add additional fields when extending a class. **Example** (Attempting to Overwrite Existing Fields) ```ts twoslash import { Schema } from "effect" class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.NonEmptyString }) { get upperName() { return this.name.toUpperCase() } } class BadExtension extends Person.extend("BadExtension")({ name: Schema.Number }) {} /* throws: Error: Duplicate property signature details: Duplicate key "name" */ ``` This error occurs because allowing fields to be overwritten is not safe. It could interfere with any getters or methods defined on the class that rely on the original definition. For example, in this case, the `upperName` getter would break if the `name` field was changed to a number. ## Transformations You can enhance schema classes with effectful transformations to enrich or validate entities, particularly when working with data sourced from external systems like databases or APIs. **Example** (Effectful Transformation) The following example demonstrates adding an `age` field to a `Person` class. The `age` value is derived asynchronously based on the `id` field. ```ts twoslash import { Effect, Option, Schema, ParseResult } from "effect" // Base class definition class Person extends Schema.Class("Person")({ id: Schema.Number, name: Schema.String }) {} console.log(Schema.decodeUnknownSync(Person)({ id: 1, name: "name" })) /* Output: Person { id: 1, name: 'name' } */ // Simulate fetching age asynchronously based on id function getAge(id: number): Effect.Effect { return Effect.succeed(id + 2) } // Extended class with a transformation class PersonWithTransform extends Person.transformOrFail( "PersonWithTransform" )( { age: Schema.optionalWith(Schema.Number, { exact: true, as: "Option" }) }, { // Decoding logic for the new field decode: (input) => Effect.mapBoth(getAge(input.id), { onFailure: (e) => new ParseResult.Type(Schema.String.ast, input.id, e.message), // Must return { age: Option } onSuccess: (age) => ({ ...input, age: Option.some(age) }) }), encode: ParseResult.succeed } ) {} Schema.decodeUnknownPromise(PersonWithTransform)({ id: 1, name: "name" }).then(console.log) /* Output: PersonWithTransform { id: 1, name: 'name', age: { _id: 'Option', _tag: 'Some', value: 3 } } */ // Extended class with a conditional Transformation class PersonWithTransformFrom extends Person.transformOrFailFrom( "PersonWithTransformFrom" )( { age: Schema.optionalWith(Schema.Number, { exact: true, as: "Option" }) }, { decode: (input) => Effect.mapBoth(getAge(input.id), { onFailure: (e) => new ParseResult.Type(Schema.String.ast, input, e.message), // Must return { age?: number } onSuccess: (age) => (age > 18 ? { ...input, age } : { ...input }) }), encode: ParseResult.succeed } ) {} Schema.decodeUnknownPromise(PersonWithTransformFrom)({ id: 1, name: "name" }).then(console.log) /* Output: PersonWithTransformFrom { id: 1, name: 'name', age: { _id: 'Option', _tag: 'None' } } */ ``` The decision of which API to use, either `transformOrFail` or `transformOrFailFrom`, depends on when you wish to execute the transformation: 1. Using `transformOrFail`: - The transformation occurs at the end of the process. - It expects you to provide a value of type `{ age: Option }`. - After processing the initial input, the new transformation comes into play, and you need to ensure the final output adheres to the specified structure. 2. Using `transformOrFailFrom`: - The new transformation starts as soon as the initial input is handled. - You should provide a value `{ age?: number }`. - Based on this fresh input, the subsequent transformation `Schema.optionalWith(Schema.Number, { exact: true, as: "Option" })` is executed. - This approach allows for immediate handling of the input, potentially influencing the subsequent transformations. # [Default Constructors](https://effect.website/docs/schema/default-constructors/) ## Overview import { Aside } from "@astrojs/starlight/components" When working with data structures, it can be helpful to create values that conform to a schema with minimal effort. For this purpose, the Schema module provides default constructors for various schema types, including `Structs`, `Records`, `filters`, and `brands`. Default constructors are **unsafe**, meaning they **throw an error** if the input does not conform to the schema. If you need a safer alternative, consider using [Schema.validateEither](#error-handling-in-constructors), which returns a result indicating success or failure instead of throwing an error. **Example** (Using a Refinement Default Constructor) ```ts twoslash import { Schema } from "effect" const schema = Schema.NumberFromString.pipe(Schema.between(1, 10)) // The constructor only accepts numbers console.log(schema.make(5)) // Output: 5 // This will throw an error because the number is outside the valid range console.log(schema.make(20)) /* throws: ParseError: between(1, 10) └─ Predicate refinement failure └─ Expected a number between 1 and 10, actual 20 */ ``` ## Structs Struct schemas allow you to define objects with specific fields and constraints. The `make` function can be used to create instances of a struct schema. **Example** (Creating Struct Instances) ```ts twoslash import { Schema } from "effect" const Struct = Schema.Struct({ name: Schema.NonEmptyString }) // Successful creation Struct.make({ name: "a" }) // This will throw an error because the name is empty Struct.make({ name: "" }) /* throws ParseError: { readonly name: NonEmptyString } └─ ["name"] └─ NonEmptyString └─ Predicate refinement failure └─ Expected NonEmptyString, actual "" */ ``` In some cases, you might need to bypass validation. While not recommended in most scenarios, `make` provides an option to disable validation. **Example** (Bypassing Validation) ```ts twoslash import { Schema } from "effect" const Struct = Schema.Struct({ name: Schema.NonEmptyString }) // Bypass validation during instantiation Struct.make({ name: "" }, true) // Or use the `disableValidation` option explicitly Struct.make({ name: "" }, { disableValidation: true }) ``` ## Records Record schemas allow you to define key-value mappings where the keys and values must meet specific criteria. **Example** (Creating Record Instances) ```ts twoslash import { Schema } from "effect" const Record = Schema.Record({ key: Schema.String, value: Schema.NonEmptyString }) // Successful creation Record.make({ a: "a", b: "b" }) // This will throw an error because 'b' is empty Record.make({ a: "a", b: "" }) /* throws ParseError: { readonly [x: string]: NonEmptyString } └─ ["b"] └─ NonEmptyString └─ Predicate refinement failure └─ Expected NonEmptyString, actual "" */ // Bypasses validation Record.make({ a: "a", b: "" }, { disableValidation: true }) ``` ## Filters Filters allow you to define constraints on individual values. **Example** (Using Filters to Enforce Ranges) ```ts twoslash import { Schema } from "effect" const MyNumber = Schema.Number.pipe(Schema.between(1, 10)) // Successful creation const n = MyNumber.make(5) // This will throw an error because the number is outside the valid range MyNumber.make(20) /* throws ParseError: a number between 1 and 10 └─ Predicate refinement failure └─ Expected a number between 1 and 10, actual 20 */ // Bypasses validation MyNumber.make(20, { disableValidation: true }) ``` ## Branded Types Branded schemas add metadata to a value to give it a more specific type, while still retaining its original type. **Example** (Creating Branded Values) ```ts twoslash import { Schema } from "effect" const BrandedNumberSchema = Schema.Number.pipe( Schema.between(1, 10), Schema.brand("MyNumber") ) // Successful creation const n = BrandedNumberSchema.make(5) // This will throw an error because the number is outside the valid range BrandedNumberSchema.make(20) /* throws ParseError: a number between 1 and 10 & Brand<"MyNumber"> └─ Predicate refinement failure └─ Expected a number between 1 and 10 & Brand<"MyNumber">, actual 20 */ // Bypasses validation BrandedNumberSchema.make(20, { disableValidation: true }) ``` When using default constructors, it is helpful to understand the type of value they produce. For instance, in the `BrandedNumberSchema` example, the return type of the constructor is `number & Brand<"MyNumber">`. This indicates that the resulting value is a `number` with additional branding information, `"MyNumber"`. This behavior contrasts with the filter example, where the return type is simply `number`. Branding adds an extra layer of type information, which can assist in identifying and working with your data more effectively. ## Error Handling in Constructors Default constructors are considered "unsafe" because they throw an error if the input does not conform to the schema. This error includes a detailed description of what went wrong. The intention behind default constructors is to provide a straightforward way to create valid values, such as for tests or configurations, where invalid inputs are expected to be exceptional cases. If you need a "safe" constructor that does not throw errors but instead returns a result indicating success or failure, you can use `Schema.validateEither`. **Example** (Using `Schema.validateEither` for Safe Validation) ```ts twoslash import { Schema } from "effect" const schema = Schema.NumberFromString.pipe(Schema.between(1, 10)) // Create a safe constructor that validates an unknown input const safeMake = Schema.validateEither(schema) // Valid input returns a Right value console.log(safeMake(5)) /* Output: { _id: 'Either', _tag: 'Right', right: 5 } */ // Invalid input returns a Left value with detailed error information console.log(safeMake(20)) /* Output: { _id: 'Either', _tag: 'Left', left: { _id: 'ParseError', message: 'between(1, 10)\n' + '└─ Predicate refinement failure\n' + ' └─ Expected a number between 1 and 10, actual 20' } } */ // This will throw an error because it's unsafe schema.make(20) /* throws: ParseError: between(1, 10) └─ Predicate refinement failure └─ Expected a number between 1 and 10, actual 20 */ ``` ## Setting Default Values When creating objects, you might want to assign default values to certain fields to simplify object construction. The `Schema.withConstructorDefault` function lets you handle default values, making fields optional in the default constructor. **Example** (Struct with Required Fields) In this example, all fields are required when creating a new instance. ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.NonEmptyString, age: Schema.Number }) // Both name and age must be provided console.log(Person.make({ name: "John", age: 30 })) /* Output: { name: 'John', age: 30 } */ ``` **Example** (Struct with Default Value) Here, the `age` field is optional because it has a default value of `0`. ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.NonEmptyString, age: Schema.Number.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => 0) ) }) // The age field is optional and defaults to 0 console.log(Person.make({ name: "John" })) /* Output: { name: 'John', age: 0 } */ console.log(Person.make({ name: "John", age: 30 })) /* Output: { name: 'John', age: 30 } */ ``` ### Nested Structs and Shallow Defaults Default values in schemas are shallow, meaning that defaults defined in nested structs do not automatically propagate to the top-level constructor. **Example** (Shallow Defaults in Nested Structs) ```ts twoslash import { Schema } from "effect" const Config = Schema.Struct({ // Define a nested struct with a default value web: Schema.Struct({ application_url: Schema.String.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => "http://localhost") ), application_port: Schema.Number }) }) // This will cause a type error because `application_url` // is missing in the nested struct // @ts-expect-error Config.make({ web: { application_port: 3000 } }) ``` This behavior occurs because the `Schema` interface does not include a type parameter to carry over default constructor types from nested structs. To work around this limitation, extract the constructor for the nested struct and apply it to its fields directly. This ensures that the nested defaults are respected. **Example** (Using Nested Struct Constructors) ```ts twoslash import { Schema } from "effect" const Config = Schema.Struct({ web: Schema.Struct({ application_url: Schema.String.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => "http://localhost") ), application_port: Schema.Number }) }) // Extract the nested struct constructor const { web: Web } = Config.fields // Use the constructor for the nested struct console.log(Config.make({ web: Web.make({ application_port: 3000 }) })) /* Output: { web: { application_url: 'http://localhost', application_port: 3000 } } */ ``` ### Lazy Evaluation of Defaults Defaults are lazily evaluated, meaning that a new instance of the default is generated every time the constructor is called: **Example** (Lazy Evaluation of Defaults) In this example, the `timestamp` field generates a new value for each instance. ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.NonEmptyString, age: Schema.Number.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => 0) ), timestamp: Schema.Number.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => new Date().getTime()) ) }) console.log(Person.make({ name: "name1" })) /* Example Output: { age: 0, timestamp: 1714232909221, name: 'name1' } */ console.log(Person.make({ name: "name2" })) /* Example Output: { age: 0, timestamp: 1714232909227, name: 'name2' } */ ``` ### Reusing Defaults Across Schemas Default values are also "portable", meaning that if you reuse the same property signature in another schema, the default is carried over: **Example** (Reusing Defaults in Another Schema) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.NonEmptyString, age: Schema.Number.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => 0) ), timestamp: Schema.Number.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => new Date().getTime()) ) }) const AnotherSchema = Schema.Struct({ foo: Schema.String, age: Person.fields.age }) console.log(AnotherSchema.make({ foo: "bar" })) /* Output: { foo: 'bar', age: 0 } */ ``` ### Using Defaults in Classes Default values can also be applied when working with the `Class` API, ensuring consistency across class-based schemas. **Example** (Defaults in a Class) ```ts twoslash import { Schema } from "effect" class Person extends Schema.Class("Person")({ name: Schema.NonEmptyString, age: Schema.Number.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => 0) ), timestamp: Schema.Number.pipe( Schema.propertySignature, Schema.withConstructorDefault(() => new Date().getTime()) ) }) {} console.log(new Person({ name: "name1" })) /* Example Output: Person { age: 0, timestamp: 1714400867208, name: 'name1' } */ console.log(new Person({ name: "name2" })) /* Example Output: Person { age: 0, timestamp: 1714400867215, name: 'name2' } */ ``` # [Effect Data Types](https://effect.website/docs/schema/effect-data-types/) ## Overview import { Aside } from "@astrojs/starlight/components" ## Interop With Data The [Data](/docs/data-types/data/) module in the Effect ecosystem simplifies value comparison by automatically implementing the [Equal](/docs/trait/equal/) and [Hash](/docs/trait/hash/) traits. This eliminates the need for manual implementations, making equality checks straightforward. **Example** (Comparing Structs with Data) ```ts twoslash import { Data, Equal } from "effect" const person1 = Data.struct({ name: "Alice", age: 30 }) const person2 = Data.struct({ name: "Alice", age: 30 }) console.log(Equal.equals(person1, person2)) // Output: true ``` By default, schemas like `Schema.Struct` do not implement the `Equal` and `Hash` traits. This means that two decoded objects with identical values will not be considered equal. **Example** (Default Behavior Without `Equal` and `Hash`) ```ts twoslash import { Schema } from "effect" import { Equal } from "effect" const schema = Schema.Struct({ name: Schema.String, age: Schema.Number }) const decode = Schema.decode(schema) const person1 = decode({ name: "Alice", age: 30 }) const person2 = decode({ name: "Alice", age: 30 }) console.log(Equal.equals(person1, person2)) // Output: false ``` The `Schema.Data` function can be used to enhance a schema by including the `Equal` and `Hash` traits. This allows the resulting objects to support value-based equality. **Example** (Using `Schema.Data` to Add Equality) ```ts twoslash import { Schema } from "effect" import { Equal } from "effect" const schema = Schema.Data( Schema.Struct({ name: Schema.String, age: Schema.Number }) ) const decode = Schema.decode(schema) const person1 = decode({ name: "Alice", age: 30 }) const person2 = decode({ name: "Alice", age: 30 }) console.log(Equal.equals(person1, person2)) // Output: true ``` ## Config The `Schema.Config` function allows you to decode and manage application configuration settings using structured schemas. It ensures consistency in configuration data and provides detailed feedback for decoding errors. **Syntax** ```ts showLineNumbers=false Config: (name: string, schema: Schema) => Config ``` This function takes two arguments: - `name`: Identifier for the configuration setting. - `schema`: Schema describing the expected data type and structure. It returns a [Config](/docs/configuration/) object that integrates with your application's configuration system. The Encoded type `I` must extend `string`, so the schema must be able to decode from a string, this includes schemas like `Schema.String`, `Schema.Literal("...")`, or `Schema.NumberFromString`, possibly with refinements applied. Behind the scenes, `Schema.Config` follows these steps: 1. **Fetch the value** using the provided name (e.g. from an environment variable). 2. **Decode the value** using the given schema. If the value is invalid, decoding fails. 3. **Format any errors** using [TreeFormatter.formatErrorSync](/docs/schema/error-formatters/#treeformatter-default), which helps produce readable and detailed error messages. **Example** (Decoding a Configuration Value) ```ts twoslash filename="config.ts" import { Effect, Schema } from "effect" // Define a config that expects a string with at least 4 characters const myConfig = Schema.Config( "Foo", Schema.String.pipe(Schema.minLength(4)) ) const program = Effect.gen(function* () { const foo = yield* myConfig console.log(`ok: ${foo}`) }) Effect.runSync(program) ``` To test the configuration, execute the following commands: **Test** (with Missing Configuration Data) ```sh showLineNumbers=false npx tsx config.ts # Output: # [(Missing data at Foo: "Expected Foo to exist in the process context")] ``` **Test** (with Invalid Data) ```sh showLineNumbers=false Foo=bar npx tsx config.ts # Output: # [(Invalid data at Foo: "a string at least 4 character(s) long # └─ Predicate refinement failure # └─ Expected a string at least 4 character(s) long, actual "bar"")] ``` **Test** (with Valid Data) ```sh showLineNumbers=false Foo=foobar npx tsx config.ts # Output: # ok: foobar ``` ## Option ### Option The `Schema.Option` function is useful for converting an `Option` into a JSON-serializable format. **Syntax** ```ts showLineNumbers=false Schema.Option(schema: Schema) ``` ##### Decoding | Input | Output | | ---------------------------- | ----------------------------------------------------------------------------------- | | `{ _tag: "None" }` | Converted to `Option.none()` | | `{ _tag: "Some", value: I }` | Converted to `Option.some(a)`, where `I` is decoded into `A` using the inner schema | ##### Encoding | Input | Output | | ---------------- | ----------------------------------------------------------------------------------------------- | | `Option.none()` | Converted to `{ _tag: "None" }` | | `Option.some(A)` | Converted to `{ _tag: "Some", value: I }`, where `A` is encoded into `I` using the inner schema | **Example** ```ts twoslash import { Schema } from "effect" import { Option } from "effect" const schema = Schema.Option(Schema.NumberFromString) // ┌─── OptionEncoded // ▼ type Encoded = typeof schema.Encoded // ┌─── Option // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log(decode({ _tag: "None" })) // Output: { _id: 'Option', _tag: 'None' } console.log(decode({ _tag: "Some", value: "1" })) // Output: { _id: 'Option', _tag: 'Some', value: 1 } // Encoding examples console.log(encode(Option.none())) // Output: { _tag: 'None' } console.log(encode(Option.some(1))) // Output: { _tag: 'Some', value: '1' } ``` ### OptionFromSelf The `Schema.OptionFromSelf` function is designed for scenarios where `Option` values are already in the `Option` format and need to be decoded or encoded while transforming the inner value according to the provided schema. **Syntax** ```ts showLineNumbers=false Schema.OptionFromSelf(schema: Schema) ``` #### Decoding | Input | Output | | ---------------- | ----------------------------------------------------------------------------------- | | `Option.none()` | Remains as `Option.none()` | | `Option.some(I)` | Converted to `Option.some(A)`, where `I` is decoded into `A` using the inner schema | #### Encoding | Input | Output | | ---------------- | ----------------------------------------------------------------------------------- | | `Option.none()` | Remains as `Option.none()` | | `Option.some(A)` | Converted to `Option.some(I)`, where `A` is encoded into `I` using the inner schema | **Example** ```ts twoslash import { Schema } from "effect" import { Option } from "effect" const schema = Schema.OptionFromSelf(Schema.NumberFromString) // ┌─── Option // ▼ type Encoded = typeof schema.Encoded // ┌─── Option // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log(decode(Option.none())) // Output: { _id: 'Option', _tag: 'None' } console.log(decode(Option.some("1"))) // Output: { _id: 'Option', _tag: 'Some', value: 1 } // Encoding examples console.log(encode(Option.none())) // Output: { _id: 'Option', _tag: 'None' } console.log(encode(Option.some(1))) // Output: { _id: 'Option', _tag: 'Some', value: '1' } ``` ### OptionFromUndefinedOr The `Schema.OptionFromUndefinedOr` function handles cases where `undefined` is treated as `Option.none()`, and all other values are interpreted as `Option.some()` based on the provided schema. **Syntax** ```ts showLineNumbers=false Schema.OptionFromUndefinedOr(schema: Schema) ``` #### Decoding | Input | Output | | ----------- | ----------------------------------------------------------------------------------- | | `undefined` | Converted to `Option.none()` | | `I` | Converted to `Option.some(A)`, where `I` is decoded into `A` using the inner schema | #### Encoding | Input | Output | | ---------------- | ---------------------------------------------------------------------- | | `Option.none()` | Converted to `undefined` | | `Option.some(A)` | Converted to `I`, where `A` is encoded into `I` using the inner schema | **Example** ```ts twoslash import { Schema } from "effect" import { Option } from "effect" const schema = Schema.OptionFromUndefinedOr(Schema.NumberFromString) // ┌─── string | undefined // ▼ type Encoded = typeof schema.Encoded // ┌─── Option // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log(decode(undefined)) // Output: { _id: 'Option', _tag: 'None' } console.log(decode("1")) // Output: { _id: 'Option', _tag: 'Some', value: 1 } // Encoding examples console.log(encode(Option.none())) // Output: undefined console.log(encode(Option.some(1))) // Output: "1" ``` ### OptionFromNullOr The `Schema.OptionFromUndefinedOr` function handles cases where `null` is treated as `Option.none()`, and all other values are interpreted as `Option.some()` based on the provided schema. **Syntax** ```ts showLineNumbers=false Schema.OptionFromNullOr(schema: Schema) ``` #### Decoding | Input | Output | | ------ | ----------------------------------------------------------------------------------- | | `null` | Converted to `Option.none()` | | `I` | Converted to `Option.some(A)`, where `I` is decoded into `A` using the inner schema | #### Encoding | Input | Output | | ---------------- | ---------------------------------------------------------------------- | | `Option.none()` | Converted to `null` | | `Option.some(A)` | Converted to `I`, where `A` is encoded into `I` using the inner schema | **Example** ```ts twoslash import { Schema } from "effect" import { Option } from "effect" const schema = Schema.OptionFromNullOr(Schema.NumberFromString) // ┌─── string | null // ▼ type Encoded = typeof schema.Encoded // ┌─── Option // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log(decode(null)) // Output: { _id: 'Option', _tag: 'None' } console.log(decode("1")) // Output: { _id: 'Option', _tag: 'Some', value: 1 } // Encoding examples console.log(encode(Option.none())) // Output: null console.log(encode(Option.some(1))) // Output: "1" ``` ### OptionFromNullishOr The `Schema.OptionFromNullishOr` function handles cases where `null` or `undefined` are treated as `Option.none()`, and all other values are interpreted as `Option.some()` based on the provided schema. Additionally, it allows customization of how `Option.none()` is encoded (`null` or `undefined`). **Syntax** ```ts showLineNumbers=false Schema.OptionFromNullishOr( schema: Schema, onNoneEncoding: null | undefined ) ``` #### Decoding | Input | Output | | ----------- | ----------------------------------------------------------------------------------- | | `undefined` | Converted to `Option.none()` | | `null` | Converted to `Option.none()` | | `I` | Converted to `Option.some(A)`, where `I` is decoded into `A` using the inner schema | #### Encoding | Input | Output | | ---------------- | -------------------------------------------------------------------------- | | `Option.none()` | Converted to `undefined` or `null` based on user choice (`onNoneEncoding`) | | `Option.some(A)` | Converted to `I`, where `A` is encoded into `I` using the inner schema | **Example** ```ts twoslash import { Schema } from "effect" import { Option } from "effect" const schema = Schema.OptionFromNullishOr( Schema.NumberFromString, undefined // Encode Option.none() as undefined ) // ┌─── string | null | undefined // ▼ type Encoded = typeof schema.Encoded // ┌─── Option // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log(decode(null)) // Output: { _id: 'Option', _tag: 'None' } console.log(decode(undefined)) // Output: { _id: 'Option', _tag: 'None' } console.log(decode("1")) // Output: { _id: 'Option', _tag: 'Some', value: 1 } // Encoding examples console.log(encode(Option.none())) // Output: undefined console.log(encode(Option.some(1))) // Output: "1" ``` ### OptionFromNonEmptyTrimmedString The `Schema.OptionFromNonEmptyTrimmedString` schema is designed for handling strings where trimmed empty strings are treated as `Option.none()`, and all other strings are converted to `Option.some()`. #### Decoding | Input | Output | | ----------- | ------------------------------------------------------- | | `s: string` | Converted to `Option.some(s)`, if `s.trim().length > 0` | | | Converted to `Option.none()` otherwise | #### Encoding | Input | Output | | ------------------------ | ----------------- | | `Option.none()` | Converted to `""` | | `Option.some(s: string)` | Converted to `s` | **Example** ```ts twoslash import { Schema, Option } from "effect" // ┌─── string // ▼ type Encoded = typeof Schema.OptionFromNonEmptyTrimmedString // ┌─── Option // ▼ type Type = typeof Schema.OptionFromNonEmptyTrimmedString const decode = Schema.decodeUnknownSync( Schema.OptionFromNonEmptyTrimmedString ) const encode = Schema.encodeSync(Schema.OptionFromNonEmptyTrimmedString) // Decoding examples console.log(decode("")) // Output: { _id: 'Option', _tag: 'None' } console.log(decode(" a ")) // Output: { _id: 'Option', _tag: 'Some', value: 'a' } console.log(decode("a")) // Output: { _id: 'Option', _tag: 'Some', value: 'a' } // Encoding examples console.log(encode(Option.none())) // Output: "" console.log(encode(Option.some("example"))) // Output: "example" ``` ## Either ### Either The `Schema.Either` function is useful for converting an `Either` into a JSON-serializable format. **Syntax** ```ts showLineNumbers=false Schema.Either(options: { left: Schema, right: Schema }) ``` ##### Decoding | Input | Output | | ------------------------------ | ----------------------------------------------------------------------------------------------- | | `{ _tag: "Left", left: LI }` | Converted to `Either.left(LA)`, where `LI` is decoded into `LA` using the inner `left` schema | | `{ _tag: "Right", right: RI }` | Converted to `Either.right(RA)`, where `RI` is decoded into `RA` using the inner `right` schema | ##### Encoding | Input | Output | | ------------------ | ----------------------------------------------------------------------------------------------------------- | | `Either.left(LA)` | Converted to `{ _tag: "Left", left: LI }`, where `LA` is encoded into `LI` using the inner `left` schema | | `Either.right(RA)` | Converted to `{ _tag: "Right", right: RI }`, where `RA` is encoded into `RI` using the inner `right` schema | **Example** ```ts twoslash import { Schema, Either } from "effect" const schema = Schema.Either({ left: Schema.Trim, right: Schema.NumberFromString }) // ┌─── EitherEncoded // ▼ type Encoded = typeof schema.Encoded // ┌─── Either // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log(decode({ _tag: "Left", left: " a " })) // Output: { _id: 'Either', _tag: 'Left', left: 'a' } console.log(decode({ _tag: "Right", right: "1" })) // Output: { _id: 'Either', _tag: 'Right', right: 1 } // Encoding examples console.log(encode(Either.left("a"))) // Output: { _tag: 'Left', left: 'a' } console.log(encode(Either.right(1))) // Output: { _tag: 'Right', right: '1' } ``` ### EitherFromSelf The `Schema.EitherFromSelf` function is designed for scenarios where `Either` values are already in the `Either` format and need to be decoded or encoded while transforming the inner valued according to the provided schemas. **Syntax** ```ts showLineNumbers=false Schema.EitherFromSelf(options: { left: Schema, right: Schema }) ``` ##### Decoding | Input | Output | | ------------------ | ----------------------------------------------------------------------------------------------- | | `Either.left(LI)` | Converted to `Either.left(LA)`, where `LI` is decoded into `LA` using the inner `left` schema | | `Either.right(RI)` | Converted to `Either.right(RA)`, where `RI` is decoded into `RA` using the inner `right` schema | ##### Encoding | Input | Output | | ------------------ | ----------------------------------------------------------------------------------------------- | | `Either.left(LA)` | Converted to `Either.left(LI)`, where `LA` is encoded into `LI` using the inner `left` schema | | `Either.right(RA)` | Converted to `Either.right(RI)`, where `RA` is encoded into `RI` using the inner `right` schema | **Example** ```ts twoslash import { Schema, Either } from "effect" const schema = Schema.EitherFromSelf({ left: Schema.Trim, right: Schema.NumberFromString }) // ┌─── Either // ▼ type Encoded = typeof schema.Encoded // ┌─── Either // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log(decode(Either.left(" a "))) // Output: { _id: 'Either', _tag: 'Left', left: 'a' } console.log(decode(Either.right("1"))) // Output: { _id: 'Either', _tag: 'Right', right: 1 } // Encoding examples console.log(encode(Either.left("a"))) // Output: { _id: 'Either', _tag: 'Left', left: 'a' } console.log(encode(Either.right(1))) // Output: { _id: 'Either', _tag: 'Right', right: '1' } ``` ### EitherFromUnion The `Schema.EitherFromUnion` function is designed to decode and encode `Either` values where the `left` and `right` sides are represented as distinct types. This schema enables conversions between raw union types and structured `Either` types. **Syntax** ```ts showLineNumbers=false Schema.EitherFromUnion(options: { left: Schema, right: Schema }) ``` ##### Decoding | Input | Output | | ----- | ----------------------------------------------------------------------------------------------- | | `LI` | Converted to `Either.left(LA)`, where `LI` is decoded into `LA` using the inner `left` schema | | `RI` | Converted to `Either.right(RA)`, where `RI` is decoded into `RA` using the inner `right` schema | ##### Encoding | Input | Output | | ------------------ | --------------------------------------------------------------------------------- | | `Either.left(LA)` | Converted to `LI`, where `LA` is encoded into `LI` using the inner `left` schema | | `Either.right(RA)` | Converted to `RI`, where `RA` is encoded into `RI` using the inner `right` schema | **Example** ```ts twoslash import { Schema, Either } from "effect" const schema = Schema.EitherFromUnion({ left: Schema.Boolean, right: Schema.NumberFromString }) // ┌─── string | boolean // ▼ type Encoded = typeof schema.Encoded // ┌─── Either // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log(decode(true)) // Output: { _id: 'Either', _tag: 'Left', left: true } console.log(decode("1")) // Output: { _id: 'Either', _tag: 'Right', right: 1 } // Encoding examples console.log(encode(Either.left(true))) // Output: true console.log(encode(Either.right(1))) // Output: "1" ``` ## Exit ### Exit The `Schema.Exit` function is useful for converting an `Exit` into a JSON-serializable format. **Syntax** ```ts showLineNumbers=false Schema.Exit(options: { failure: Schema, success: Schema, defect: Schema }) ``` ##### Decoding | Input | Output | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `{ _tag: "Failure", cause: CauseEncoded }` | Converted to `Exit.failCause(Cause)`, where `CauseEncoded` is decoded into `Cause` using the inner `failure` and `defect` schemas | | `{ _tag: "Success", value: SI }` | Converted to `Exit.succeed(SA)`, where `SI` is decoded into `SA` using the inner `success` schema | ##### Encoding | Input | Output | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Exit.failCause(Cause)` | Converted to `{ _tag: "Failure", cause: CauseEncoded }`, where `Cause` is encoded into `CauseEncoded` using the inner `failure` and `defect` schemas | | `Exit.succeed(SA)` | Converted to `{ _tag: "Success", value: SI }`, where `SA` is encoded into `SI` using the inner `success` schema | **Example** ```ts twoslash import { Schema, Exit } from "effect" const schema = Schema.Exit({ failure: Schema.String, success: Schema.NumberFromString, defect: Schema.String }) // ┌─── ExitEncoded // ▼ type Encoded = typeof schema.Encoded // ┌─── Exit // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log( decode({ _tag: "Failure", cause: { _tag: "Fail", error: "a" } }) ) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'a' } } */ console.log(decode({ _tag: "Success", value: "1" })) /* Output: { _id: 'Exit', _tag: 'Success', value: 1 } */ // Encoding examples console.log(encode(Exit.fail("a"))) /* Output: { _tag: 'Failure', cause: { _tag: 'Fail', error: 'a' } } */ console.log(encode(Exit.succeed(1))) /* Output: { _tag: 'Success', value: '1' } */ ``` ### Handling Defects in Serialization Effect provides a built-in `Defect` schema to handle JavaScript errors (`Error` instances) and other types of unrecoverable defects. - When decoding, it reconstructs `Error` instances if the input has a `message` and optionally a `name` and `stack`. - When encoding, it converts `Error` instances into plain objects that retain only essential properties. This is useful when transmitting errors across network requests or logging systems where `Error` objects do not serialize by default. **Example** (Encoding and Decoding Defects) ```ts twoslash import { Schema, Exit } from "effect" const schema = Schema.Exit({ failure: Schema.String, success: Schema.NumberFromString, defect: Schema.Defect }) const decode = Schema.decodeSync(schema) const encode = Schema.encodeSync(schema) console.log(encode(Exit.die(new Error("Message")))) /* Output: { _tag: 'Failure', cause: { _tag: 'Die', defect: { name: 'Error', message: 'Message' } } } */ console.log(encode(Exit.fail("a"))) console.log( decode({ _tag: "Failure", cause: { _tag: "Die", defect: { name: "Error", message: "Message" } } }) ) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Die', defect: [Error: Message] { [cause]: [Object] } } } */ ``` ### ExitFromSelf The `Schema.ExitFromSelf` function is designed for scenarios where `Exit` values are already in the `Exit` format and need to be decoded or encoded while transforming the inner valued according to the provided schemas. **Syntax** ```ts showLineNumbers=false Schema.ExitFromSelf(options: { failure: Schema, success: Schema, defect: Schema }) ``` ##### Decoding | Input | Output | | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `Exit.failCause(Cause)` | Converted to `Exit.failCause(Cause)`, where `Cause` is decoded into `Cause` using the inner `failure` and `defect` schemas | | `Exit.succeed(SI)` | Converted to `Exit.succeed(SA)`, where `SI` is decoded into `SA` using the inner `success` schema | ##### Encoding | Input | Output | | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `Exit.failCause(Cause)` | Converted to `Exit.failCause(Cause)`, where `Cause` is decoded into `Cause` using the inner `failure` and `defect` schemas | | `Exit.succeed(SA)` | Converted to `Exit.succeed(SI)`, where `SA` is encoded into `SI` using the inner `success` schema | **Example** ```ts twoslash import { Schema, Exit } from "effect" const schema = Schema.ExitFromSelf({ failure: Schema.String, success: Schema.NumberFromString, defect: Schema.String }) // ┌─── Exit // ▼ type Encoded = typeof schema.Encoded // ┌─── Exit // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log(decode(Exit.fail("a"))) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'a' } } */ console.log(decode(Exit.succeed("1"))) /* Output: { _id: 'Exit', _tag: 'Success', value: 1 } */ // Encoding examples console.log(encode(Exit.fail("a"))) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'a' } } */ console.log(encode(Exit.succeed(1))) /* Output: { _id: 'Exit', _tag: 'Success', value: '1' } */ ``` ## ReadonlySet ### ReadonlySet The `Schema.ReadonlySet` function is useful for converting a `ReadonlySet` into a JSON-serializable format. **Syntax** ```ts showLineNumbers=false Schema.ReadonlySet(schema: Schema) ``` ##### Decoding | Input | Output | | ------------------ | ----------------------------------------------------------------------------------- | | `ReadonlyArray` | Converted to `ReadonlySet`, where `I` is decoded into `A` using the inner schema | ##### Encoding | Input | Output | | ---------------- | ------------------------------------------------------------------------ | | `ReadonlySet` | `ReadonlyArray`, where `A` is encoded into `I` using the inner schema | **Example** ```ts twoslash import { Schema } from "effect" const schema = Schema.ReadonlySet(Schema.NumberFromString) // ┌─── readonly string[] // ▼ type Encoded = typeof schema.Encoded // ┌─── ReadonlySet // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log(decode(["1", "2", "3"])) // Output: Set(3) { 1, 2, 3 } // Encoding examples console.log(encode(new Set([1, 2, 3]))) // Output: [ '1', '2', '3' ] ``` ### ReadonlySetFromSelf The `Schema.ReadonlySetFromSelf` function is designed for scenarios where `ReadonlySet` values are already in the `ReadonlySet` format and need to be decoded or encoded while transforming the inner values according to the provided schema. **Syntax** ```ts showLineNumbers=false Schema.ReadonlySetFromSelf(schema: Schema) ``` ##### Decoding | Input | Output | | ---------------- | ----------------------------------------------------------------------------------- | | `ReadonlySet` | Converted to `ReadonlySet`, where `I` is decoded into `A` using the inner schema | ##### Encoding | Input | Output | | ---------------- | ---------------------------------------------------------------------- | | `ReadonlySet` | `ReadonlySet`, where `A` is encoded into `I` using the inner schema | **Example** ```ts twoslash import { Schema } from "effect" const schema = Schema.ReadonlySetFromSelf(Schema.NumberFromString) // ┌─── ReadonlySet // ▼ type Encoded = typeof schema.Encoded // ┌─── ReadonlySet // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log(decode(new Set(["1", "2", "3"]))) // Output: Set(3) { 1, 2, 3 } // Encoding examples console.log(encode(new Set([1, 2, 3]))) // Output: Set(3) { '1', '2', '3' } ``` ## ReadonlyMap The `Schema.ReadonlyMap` function is useful for converting a `ReadonlyMap` into a JSON-serializable format. ### ReadonlyMap **Syntax** ```ts showLineNumbers=false Schema.ReadonlyMap(options: { key: Schema, value: Schema }) ``` ##### Decoding | Input | Output | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ReadonlyArray` | Converted to `ReadonlyMap`, where `KI` is decoded into `KA` using the inner `key` schema and `VI` is decoded into `VA` using the inner `value` schema | ##### Encoding | Input | Output | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ReadonlyMap` | Converted to `ReadonlyArray`, where `KA` is decoded into `KI` using the inner `key` schema and `VA` is decoded into `VI` using the inner `value` schema | **Example** ```ts twoslash import { Schema } from "effect" const schema = Schema.ReadonlyMap({ key: Schema.String, value: Schema.NumberFromString }) // ┌─── readonly (readonly [string, string])[] // ▼ type Encoded = typeof schema.Encoded // ┌─── ReadonlyMap // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log( decode([ ["a", "2"], ["b", "2"], ["c", "3"] ]) ) // Output: Map(3) { 'a' => 2, 'b' => 2, 'c' => 3 } // Encoding examples console.log( encode( new Map([ ["a", 1], ["b", 2], ["c", 3] ]) ) ) // Output: [ [ 'a', '1' ], [ 'b', '2' ], [ 'c', '3' ] ] ``` ### ReadonlyMapFromSelf The `Schema.ReadonlyMapFromSelf` function is designed for scenarios where `ReadonlyMap` values are already in the `ReadonlyMap` format and need to be decoded or encoded while transforming the inner values according to the provided schemas. **Syntax** ```ts showLineNumbers=false Schema.ReadonlyMapFromSelf(options: { key: Schema, value: Schema }) ``` ##### Decoding | Input | Output | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ReadonlyMap` | Converted to `ReadonlyMap`, where `KI` is decoded into `KA` using the inner `key` schema and `VI` is decoded into `VA` using the inner `value` schema | ##### Encoding | Input | Output | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ReadonlyMap` | Converted to `ReadonlyMap`, where `KA` is decoded into `KI` using the inner `key` schema and `VA` is decoded into `VI` using the inner `value` schema | **Example** ```ts twoslash import { Schema } from "effect" const schema = Schema.ReadonlyMapFromSelf({ key: Schema.String, value: Schema.NumberFromString }) // ┌─── ReadonlyMap // ▼ type Encoded = typeof schema.Encoded // ┌─── ReadonlyMap // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log( decode( new Map([ ["a", "2"], ["b", "2"], ["c", "3"] ]) ) ) // Output: Map(3) { 'a' => 2, 'b' => 2, 'c' => 3 } // Encoding examples console.log( encode( new Map([ ["a", 1], ["b", 2], ["c", 3] ]) ) ) // Output: Map(3) { 'a' => '1', 'b' => '2', 'c' => '3' } ``` ### ReadonlyMapFromRecord The `Schema.ReadonlyMapFromRecord` function is a utility to transform a `ReadonlyMap` into an object format, where keys are strings and values are serializable, and vice versa. **Syntax** ```ts showLineNumbers=false Schema.ReadonlyMapFromRecord({ key: Schema, value: Schema }) ``` #### Decoding | Input | Output | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | | `{ readonly [x: string]: VI }` | Converts to `ReadonlyMap`, where `x` is decoded into `KA` using the `key` schema and `VI` into `VA` using the `value` schema | #### Encoding | Input | Output | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `ReadonlyMap` | Converts to `{ readonly [x: string]: VI }`, where `KA` is encoded into `x` using the `key` schema and `VA` into `VI` using the `value` schema | **Example** ```ts twoslash import { Schema } from "effect" const schema = Schema.ReadonlyMapFromRecord({ key: Schema.NumberFromString, value: Schema.NumberFromString }) // ┌─── { readonly [x: string]: string; } // ▼ type Encoded = typeof schema.Encoded // ┌─── ReadonlyMap // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log( decode({ "1": "4", "2": "5", "3": "6" }) ) // Output: Map(3) { 1 => 4, 2 => 5, 3 => 6 } // Encoding examples console.log( encode( new Map([ [1, 4], [2, 5], [3, 6] ]) ) ) // Output: { '1': '4', '2': '5', '3': '6' } ``` ## HashSet ### HashSet The `Schema.HashSet` function provides a way to map between `HashSet` and an array representation, allowing for JSON serialization and deserialization. **Syntax** ```ts showLineNumbers=false Schema.HashSet(schema: Schema) ``` #### Decoding | Input | Output | | ------------------ | --------------------------------------------------------------------------------------------------- | | `ReadonlyArray` | Converts to `HashSet`, where each element in the array is decoded into type `A` using the schema | #### Encoding | Input | Output | | ------------ | ------------------------------------------------------------------------------------------------------------- | | `HashSet` | Converts to `ReadonlyArray`, where each element in the `HashSet` is encoded into type `I` using the schema | **Example** ```ts twoslash import { Schema } from "effect" import { HashSet } from "effect" const schema = Schema.HashSet(Schema.NumberFromString) // ┌─── readonly string[] // ▼ type Encoded = typeof schema.Encoded // ┌─── HashSet // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log(decode(["1", "2", "3"])) // Output: { _id: 'HashSet', values: [ 1, 2, 3 ] } // Encoding examples console.log(encode(HashSet.fromIterable([1, 2, 3]))) // Output: [ '1', '2', '3' ] ``` ### HashSetFromSelf The `Schema.HashSetFromSelf` function is designed for scenarios where `HashSet` values are already in the `HashSet` format and need to be decoded or encoded while transforming the inner values according to the provided schema. **Syntax** ```ts showLineNumbers=false Schema.HashSetFromSelf(schema: Schema) ``` #### Decoding | Input | Output | | ------------ | ------------------------------------------------------------------------------------------ | | `HashSet` | Converts to `HashSet`, decoding each element from type `I` to type `A` using the schema | #### Encoding | Input | Output | | ------------ | ------------------------------------------------------------------------------------------ | | `HashSet` | Converts to `HashSet`, encoding each element from type `A` to type `I` using the schema | **Example** ```ts twoslash import { Schema } from "effect" import { HashSet } from "effect" const schema = Schema.HashSetFromSelf(Schema.NumberFromString) // ┌─── HashSet // ▼ type Encoded = typeof schema.Encoded // ┌─── HashSet // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log(decode(HashSet.fromIterable(["1", "2", "3"]))) // Output: { _id: 'HashSet', values: [ 1, 2, 3 ] } // Encoding examples console.log(encode(HashSet.fromIterable([1, 2, 3]))) // Output: { _id: 'HashSet', values: [ '1', '3', '2' ] } ``` ## HashMap ### HashMap The `Schema.HashMap` function is useful for converting a `HashMap` into a JSON-serializable format. **Syntax** ```ts showLineNumbers=false Schema.HashMap(options: { key: Schema, value: Schema }) ``` | Input | Output | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `ReadonlyArray` | Converts to `HashMap`, where `KI` is decoded into `KA` and `VI` is decoded into `VA` using the specified schemas | #### Encoding | Input | Output | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `HashMap` | Converts to `ReadonlyArray`, where `KA` is encoded into `KI` and `VA` is encoded into `VI` using the specified schemas | **Example** ```ts twoslash import { Schema } from "effect" import { HashMap } from "effect" const schema = Schema.HashMap({ key: Schema.String, value: Schema.NumberFromString }) // ┌─── readonly (readonly [string, string])[] // ▼ type Encoded = typeof schema.Encoded // ┌─── HashMap // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log( decode([ ["a", "2"], ["b", "2"], ["c", "3"] ]) ) // Output: { _id: 'HashMap', values: [ [ 'a', 2 ], [ 'c', 3 ], [ 'b', 2 ] ] } // Encoding examples console.log( encode( HashMap.fromIterable([ ["a", 1], ["b", 2], ["c", 3] ]) ) ) // Output: [ [ 'a', '1' ], [ 'c', '3' ], [ 'b', '2' ] ] ``` ### HashMapFromSelf The `Schema.HashMapFromSelf` function is designed for scenarios where `HashMap` values are already in the `HashMap` format and need to be decoded or encoded while transforming the inner values according to the provided schemas. **Syntax** ```ts showLineNumbers=false Schema.HashMapFromSelf(options: { key: Schema, value: Schema }) ``` #### Decoding | Input | Output | | ----------------- | ------------------------------------------------------------------------------------------------------------------------ | | `HashMap` | Converts to `HashMap`, where `KI` is decoded into `KA` and `VI` is decoded into `VA` using the specified schemas | #### Encoding | Input | Output | | ----------------- | ------------------------------------------------------------------------------------------------------------------------ | | `HashMap` | Converts to `HashMap`, where `KA` is encoded into `KI` and `VA` is encoded into `VI` using the specified schemas | **Example** ```ts twoslash import { Schema } from "effect" import { HashMap } from "effect" const schema = Schema.HashMapFromSelf({ key: Schema.String, value: Schema.NumberFromString }) // ┌─── HashMap // ▼ type Encoded = typeof schema.Encoded // ┌─── HashMap // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log( decode( HashMap.fromIterable([ ["a", "2"], ["b", "2"], ["c", "3"] ]) ) ) // Output: { _id: 'HashMap', values: [ [ 'a', 2 ], [ 'c', 3 ], [ 'b', 2 ] ] } // Encoding examples console.log( encode( HashMap.fromIterable([ ["a", 1], ["b", 2], ["c", 3] ]) ) ) // Output: { _id: 'HashMap', values: [ [ 'a', '1' ], [ 'c', '3' ], [ 'b', '2' ] ] } ``` ## SortedSet ### SortedSet The `Schema.SortedSet` function provides a way to map between `SortedSet` and an array representation, allowing for JSON serialization and deserialization. **Syntax** ```ts showLineNumbers=false Schema.SortedSet(schema: Schema, order: Order) ``` #### Decoding | Input | Output | | ------------------ | ----------------------------------------------------------------------------------------------------- | | `ReadonlyArray` | Converts to `SortedSet`, where each element in the array is decoded into type `A` using the schema | #### Encoding | Input | Output | | -------------- | --------------------------------------------------------------------------------------------------------------- | | `SortedSet` | Converts to `ReadonlyArray`, where each element in the `SortedSet` is encoded into type `I` using the schema | **Example** ```ts twoslash import { Schema } from "effect" import { Number, SortedSet } from "effect" const schema = Schema.SortedSet(Schema.NumberFromString, Number.Order) // ┌─── readonly string[] // ▼ type Encoded = typeof schema.Encoded // ┌─── SortedSet // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log(decode(["1", "2", "3"])) // Output: { _id: 'SortedSet', values: [ 1, 2, 3 ] } // Encoding examples console.log(encode(SortedSet.fromIterable(Number.Order)([1, 2, 3]))) // Output: [ '1', '2', '3' ] ``` ### SortedSetFromSelf The `Schema.SortedSetFromSelf` function is designed for scenarios where `SortedSet` values are already in the `SortedSet` format and need to be decoded or encoded while transforming the inner values according to the provided schema. **Syntax** ```ts showLineNumbers=false Schema.SortedSetFromSelf( schema: Schema, decodeOrder: Order, encodeOrder: Order ) ``` #### Decoding | Input | Output | | -------------- | -------------------------------------------------------------------------------------------- | | `SortedSet` | Converts to `SortedSet`, decoding each element from type `I` to type `A` using the schema | #### Encoding | Input | Output | | -------------- | -------------------------------------------------------------------------------------------- | | `SortedSet` | Converts to `SortedSet`, encoding each element from type `A` to type `I` using the schema | **Example** ```ts twoslash import { Schema } from "effect" import { Number, SortedSet, String } from "effect" const schema = Schema.SortedSetFromSelf( Schema.NumberFromString, Number.Order, String.Order ) // ┌─── SortedSet // ▼ type Encoded = typeof schema.Encoded // ┌─── SortedSet // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) const encode = Schema.encodeSync(schema) // Decoding examples console.log(decode(SortedSet.fromIterable(String.Order)(["1", "2", "3"]))) // Output: { _id: 'SortedSet', values: [ 1, 2, 3 ] } // Encoding examples console.log(encode(SortedSet.fromIterable(Number.Order)([1, 2, 3]))) // Output: { _id: 'SortedSet', values: [ '1', '2', '3' ] } ``` ## Duration The `Duration` schema family enables the transformation and validation of duration values across various formats, including `hrtime`, milliseconds, and nanoseconds. ### Duration Converts an hrtime(i.e. `[seconds: number, nanos: number]`) into a `Duration`. **Example** ```ts twoslash import { Schema } from "effect" const schema = Schema.Duration // ┌─── readonly [seconds: number, nanos: number] // ▼ type Encoded = typeof schema.Encoded // ┌─── Duration // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) // Decoding examples console.log(decode([0, 0])) // Output: { _id: 'Duration', _tag: 'Millis', millis: 0 } console.log(decode([5000, 0])) // Output: { _id: 'Duration', _tag: 'Nanos', hrtime: [ 5000, 0 ] } ``` ### DurationFromSelf The `DurationFromSelf` schema is designed to validate that a given value conforms to the `Duration` type. **Example** ```ts twoslash import { Schema, Duration } from "effect" const schema = Schema.DurationFromSelf // ┌─── Duration // ▼ type Encoded = typeof schema.Encoded // ┌─── Duration // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) // Decoding examples console.log(decode(Duration.seconds(2))) // Output: { _id: 'Duration', _tag: 'Millis', millis: 2000 } console.log(decode(null)) /* throws: ParseError: Expected DurationFromSelf, actual null */ ``` ### DurationFromMillis Converts a `number` into a `Duration` where the number represents the number of milliseconds. **Example** ```ts twoslash import { Schema } from "effect" const schema = Schema.DurationFromMillis // ┌─── number // ▼ type Encoded = typeof schema.Encoded // ┌─── Duration // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) // Decoding examples console.log(decode(0)) // Output: { _id: 'Duration', _tag: 'Millis', millis: 0 } console.log(decode(5000)) // Output: { _id: 'Duration', _tag: 'Millis', millis: 5000 } ``` ### DurationFromNanos Converts a `BigInt` into a `Duration` where the number represents the number of nanoseconds. **Example** ```ts twoslash import { Schema } from "effect" const schema = Schema.DurationFromNanos // ┌─── bigint // ▼ type Encoded = typeof schema.Encoded // ┌─── Duration // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) // Decoding examples console.log(decode(0n)) // Output: { _id: 'Duration', _tag: 'Millis', millis: 0 } console.log(decode(5000000000n)) // Output: { _id: 'Duration', _tag: 'Nanos', hrtime: [ 5, 0 ] } ``` ### clampDuration Clamps a `Duration` between a minimum and a maximum value. **Example** ```ts twoslash import { Schema, Duration } from "effect" const schema = Schema.DurationFromSelf.pipe( Schema.clampDuration("5 seconds", "10 seconds") ) // ┌─── Duration // ▼ type Encoded = typeof schema.Encoded // ┌─── Duration // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) // Decoding examples console.log(decode(Duration.decode("2 seconds"))) // Output: { _id: 'Duration', _tag: 'Millis', millis: 5000 } console.log(decode(Duration.decode("6 seconds"))) // Output: { _id: 'Duration', _tag: 'Millis', millis: 6000 } console.log(decode(Duration.decode("11 seconds"))) // Output: { _id: 'Duration', _tag: 'Millis', millis: 10000 } ``` ## Redacted ### Redacted The `Schema.Redacted` function is specifically designed to handle sensitive information by converting a `string` into a [Redacted](/docs/data-types/redacted/) object. This transformation ensures that the sensitive data is not exposed in the application's output. **Example** (Basic Redacted Schema) ```ts twoslash import { Schema } from "effect" const schema = Schema.Redacted(Schema.String) // ┌─── string // ▼ type Encoded = typeof schema.Encoded // ┌─── Redacted // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) // Decoding examples console.log(decode("keep it secret, keep it safe")) // Output: ``` It's important to note that when successfully decoding a `Redacted`, the output is intentionally obscured (``) to prevent the actual secret from being revealed in logs or console outputs. **Example** (Exposure Risks During Errors) In the example below, if the input string does not meet the criteria (e.g., contains spaces), the error message generated might inadvertently expose sensitive information included in the input. ```ts twoslash import { Schema } from "effect" import { Redacted } from "effect" const schema = Schema.Trimmed.pipe( Schema.compose(Schema.Redacted(Schema.String)) ) console.log(Schema.decodeUnknownEither(schema)(" SECRET")) /* { _id: 'Either', _tag: 'Left', left: { _id: 'ParseError', message: '(Trimmed <-> (string <-> Redacted()))\n' + '└─ Encoded side transformation failure\n' + ' └─ Trimmed\n' + ' └─ Predicate refinement failure\n' + ' └─ Expected Trimmed (a string with no leading or trailing whitespace), actual " SECRET"' } } */ console.log(Schema.encodeEither(schema)(Redacted.make(" SECRET"))) /* { _id: 'Either', _tag: 'Left', left: { _id: 'ParseError', message: '(Trimmed <-> (string <-> Redacted()))\n' + '└─ Encoded side transformation failure\n' + ' └─ Trimmed\n' + ' └─ Predicate refinement failure\n' + ' └─ Expected Trimmed (a string with no leading or trailing whitespace), actual " SECRET"' } } */ ``` #### Mitigating Exposure Risks To reduce the risk of sensitive information leakage in error messages, you can customize the error messages to obscure sensitive details: **Example** (Customizing Error Messages) ```ts twoslash import { Schema } from "effect" import { Redacted } from "effect" const schema = Schema.Trimmed.annotations({ message: () => "Expected Trimmed, actual " }).pipe(Schema.compose(Schema.Redacted(Schema.String))) console.log(Schema.decodeUnknownEither(schema)(" SECRET")) /* { _id: 'Either', _tag: 'Left', left: { _id: 'ParseError', message: '(Trimmed <-> (string <-> Redacted()))\n' + '└─ Encoded side transformation failure\n' + ' └─ Expected Trimmed, actual ' } } */ console.log(Schema.encodeEither(schema)(Redacted.make(" SECRET"))) /* { _id: 'Either', _tag: 'Left', left: { _id: 'ParseError', message: '(Trimmed <-> (string <-> Redacted()))\n' + '└─ Encoded side transformation failure\n' + ' └─ Expected Trimmed, actual ' } } */ ``` ### RedactedFromSelf The `Schema.RedactedFromSelf` schema is designed to validate that a given value conforms to the `Redacted` type from the `effect` library. **Example** ```ts twoslash import { Schema } from "effect" import { Redacted } from "effect" const schema = Schema.RedactedFromSelf(Schema.String) // ┌─── Redacted // ▼ type Encoded = typeof schema.Encoded // ┌─── Redacted // ▼ type Type = typeof schema.Type const decode = Schema.decodeUnknownSync(schema) // Decoding examples console.log(decode(Redacted.make("mysecret"))) // Output: console.log(decode(null)) /* throws: ParseError: Expected Redacted(), actual null */ ``` It's important to note that when successfully decoding a `Redacted`, the output is intentionally obscured (``) to prevent the actual secret from being revealed in logs or console outputs. # [Schema to Equivalence](https://effect.website/docs/schema/equivalence/) ## Overview The `Schema.equivalence` function allows you to generate an [Equivalence](/docs/schema/equivalence/) based on a schema definition. This function is designed to compare data structures for equivalence according to the rules defined in the schema. **Example** (Comparing Structs for Equivalence) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) // Generate an equivalence function based on the schema const PersonEquivalence = Schema.equivalence(Person) const john = { name: "John", age: 23 } const alice = { name: "Alice", age: 30 } // Use the equivalence function to compare objects console.log(PersonEquivalence(john, { name: "John", age: 23 })) // Output: true console.log(PersonEquivalence(john, alice)) // Output: false ``` ## Equivalence for Any, Unknown, and Object When working with the following schemas: - `Schema.Any` - `Schema.Unknown` - `Schema.Object` - `Schema.Struct({})` (representing the broad `{}` TypeScript type) the most sensible form of equivalence is to use `Equal.equals` from the [Equal](/docs/trait/equal/) module, which defaults to reference equality (`===`). This is because these types can hold almost any kind of value. **Example** (Comparing Empty Objects Using Reference Equality) ```ts twoslash import { Schema } from "effect" const schema = Schema.Struct({}) const input1 = {} const input2 = {} console.log(Schema.equivalence(schema)(input1, input2)) // Output: false (because they are different references) ``` ## Customizing Equivalence Generation You can customize the equivalence logic by providing an `equivalence` annotation in the schema definition. The `equivalence` annotation takes any type parameters provided (`typeParameters`) and two values for comparison, returning a boolean based on the desired condition of equivalence. **Example** (Custom Equivalence for Strings) ```ts twoslash import { Schema } from "effect" // Define a schema with a custom equivalence annotation const schema = Schema.String.annotations({ equivalence: (/**typeParameters**/) => (s1, s2) => // Custom rule: Compare only the first character of the strings s1.charAt(0) === s2.charAt(0) }) // Generate the equivalence function const customEquivalence = Schema.equivalence(schema) // Use the custom equivalence function console.log(customEquivalence("aaa", "abb")) // Output: true (both start with 'a') console.log(customEquivalence("aaa", "bba")) // Output: false (strings start with different characters) ``` # [Error Formatters](https://effect.website/docs/schema/error-formatters/) ## Overview When working with Effect Schema, errors encountered during decoding or encoding operations can be formatted using two built-in methods: `TreeFormatter` and `ArrayFormatter`. These formatters help structure and present errors in a readable and actionable manner. ## TreeFormatter (default) The `TreeFormatter` is the default method for formatting errors. It organizes errors in a tree structure, providing a clear hierarchy of issues. **Example** (Decoding with Missing Properties) ```ts twoslash import { Either, Schema, ParseResult } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) const decode = Schema.decodeUnknownEither(Person) const result = decode({}) if (Either.isLeft(result)) { console.error("Decoding failed:") console.error(ParseResult.TreeFormatter.formatErrorSync(result.left)) } /* Decoding failed: { readonly name: string; readonly age: number } └─ ["name"] └─ is missing */ ``` In this example: - `{ readonly name: string; readonly age: number }` describes the schema's expected structure. - `["name"]` identifies the specific field causing the error. - `is missing` explains the issue for the `"name"` field. ### Customizing the Output You can make the error output more concise and meaningful by annotating the schema with annotations like `identifier`, `title`, or `description`. These annotations replace the default TypeScript-like representation in the error messages. **Example** (Using `title` Annotation for Clarity) Adding a `title` annotation replaces the schema structure in the error message with the more human-readable "Person" making it easier to understand. ```ts twoslash import { Either, Schema, ParseResult } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }).annotations({ title: "Person" }) // Add a title annotation const result = Schema.decodeUnknownEither(Person)({}) if (Either.isLeft(result)) { console.error(ParseResult.TreeFormatter.formatErrorSync(result.left)) } /* Person └─ ["name"] └─ is missing */ ``` ### Handling Multiple Errors By default, decoding functions like `Schema.decodeUnknownEither` report only the first error. To list all errors, use the `{ errors: "all" }` option. **Example** (Listing All Errors) ```ts twoslash import { Either, Schema, ParseResult } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) const decode = Schema.decodeUnknownEither(Person, { errors: "all" }) const result = decode({}) if (Either.isLeft(result)) { console.error("Decoding failed:") console.error(ParseResult.TreeFormatter.formatErrorSync(result.left)) } /* Decoding failed: { readonly name: string; readonly age: number } ├─ ["name"] │ └─ is missing └─ ["age"] └─ is missing */ ``` ### ParseIssueTitle Annotation The `parseIssueTitle` annotation allows you to add dynamic context to error messages by generating titles based on the value being validated. For instance, it can include an ID from the validated object, making it easier to identify specific issues in complex or nested data structures. **Annotation Type** ```ts export type ParseIssueTitleAnnotation = ( issue: ParseIssue ) => string | undefined ``` **Return Value**: - If the function returns a `string`, the `TreeFormatter` uses it as the title unless a `message` annotation is present (which takes precedence). - If the function returns `undefined`, the `TreeFormatter` determines the title based on the following priority: 1. `identifier` annotation 2. `title` annotation 3. `description` annotation 4. Default TypeScript-like schema representation **Example** (Dynamic Titles Using `parseIssueTitle`) ```ts twoslash import type { ParseResult } from "effect" import { Schema } from "effect" // Function to generate titles for OrderItem issues const getOrderItemId = ({ actual }: ParseResult.ParseIssue) => { if (Schema.is(Schema.Struct({ id: Schema.String }))(actual)) { return `OrderItem with id: ${actual.id}` } } const OrderItem = Schema.Struct({ id: Schema.String, name: Schema.String, price: Schema.Number }).annotations({ identifier: "OrderItem", parseIssueTitle: getOrderItemId }) // Function to generate titles for Order issues const getOrderId = ({ actual }: ParseResult.ParseIssue) => { if (Schema.is(Schema.Struct({ id: Schema.Number }))(actual)) { return `Order with id: ${actual.id}` } } const Order = Schema.Struct({ id: Schema.Number, name: Schema.String, items: Schema.Array(OrderItem) }).annotations({ identifier: "Order", parseIssueTitle: getOrderId }) const decode = Schema.decodeUnknownSync(Order, { errors: "all" }) // Case 1: No id available, uses the `identifier` annotation decode({}) /* throws ParseError: Order ├─ ["id"] │ └─ is missing ├─ ["name"] │ └─ is missing └─ ["items"] └─ is missing */ // Case 2: ID present, uses the dynamic `parseIssueTitle` annotation decode({ id: 1 }) /* throws ParseError: Order with id: 1 ├─ ["name"] │ └─ is missing └─ ["items"] └─ is missing */ // Case 3: Nested issues with IDs for both Order and OrderItem decode({ id: 1, items: [{ id: "22b", price: "100" }] }) /* throws ParseError: Order with id: 1 ├─ ["name"] │ └─ is missing └─ ["items"] └─ ReadonlyArray └─ [0] └─ OrderItem with id: 22b ├─ ["name"] │ └─ is missing └─ ["price"] └─ Expected a number, actual "100" */ ``` ## ArrayFormatter The `ArrayFormatter` provides a structured, array-based approach to formatting errors. It represents each error as an object, making it easier to analyze and address multiple issues during data decoding or encoding. Each error object includes properties like `_tag`, `path`, and `message` for clarity. **Example** (Single Error in Array Format) ```ts twoslash import { Either, Schema, ParseResult } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) const decode = Schema.decodeUnknownEither(Person) const result = decode({}) if (Either.isLeft(result)) { console.error("Decoding failed:") console.error(ParseResult.ArrayFormatter.formatErrorSync(result.left)) } /* Decoding failed: [ { _tag: 'Missing', path: [ 'name' ], message: 'is missing' } ] */ ``` In this example: - `_tag`: Indicates the type of error (`Missing`). - `path`: Specifies the location of the error in the data (`['name']`). - `message`: Describes the issue (`'is missing'`). ### Handling Multiple Errors By default, decoding functions like `Schema.decodeUnknownEither` report only the first error. To list all errors, use the `{ errors: "all" }` option. **Example** (Listing All Errors) ```ts twoslash import { Either, Schema, ParseResult } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) const decode = Schema.decodeUnknownEither(Person, { errors: "all" }) const result = decode({}) if (Either.isLeft(result)) { console.error("Decoding failed:") console.error(ParseResult.ArrayFormatter.formatErrorSync(result.left)) } /* Decoding failed: [ { _tag: 'Missing', path: [ 'name' ], message: 'is missing' }, { _tag: 'Missing', path: [ 'age' ], message: 'is missing' } ] */ ``` ## React Hook Form If you are working with React and need form validation, `@hookform/resolvers` offers an adapter for `effect/Schema`, which can be integrated with React Hook Form for enhanced form validation processes. This integration allows you to leverage the powerful features of `effect/Schema` within your React applications. For more detailed instructions and examples on how to integrate `effect/Schema` with React Hook Form using `@hookform/resolvers`, you can visit the official npm package page: [React Hook Form Resolvers](https://www.npmjs.com/package/@hookform/resolvers#effect-ts) # [Error Messages](https://effect.website/docs/schema/error-messages/) ## Overview ## Default Error Messages By default, when a parsing error occurs, the system automatically generates an informative message based on the schema's structure and the nature of the error (see [TreeFormatter](/docs/schema/error-formatters/#treeformatter-default) for more informations). For example, if a required property is missing or a data type does not match, the error message will clearly state the expectation versus the actual input. **Example** (Type Mismatch) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) Schema.decodeUnknownSync(Person)(null) // Output: ParseError: Expected { readonly name: string; readonly age: number }, actual null ``` **Example** (Missing Properties) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) Schema.decodeUnknownSync(Person)({}, { errors: "all" }) /* throws: ParseError: { readonly name: string; readonly age: number } ├─ ["name"] │ └─ is missing └─ ["age"] └─ is missing */ ``` **Example** (Incorrect Property Type) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) Schema.decodeUnknownSync(Person)( { name: null, age: "age" }, { errors: "all" } ) /* throws: ParseError: { readonly name: string; readonly age: number } ├─ ["name"] │ └─ Expected string, actual null └─ ["age"] └─ Expected number, actual "age" */ ``` ### Enhancing Clarity in Error Messages with Identifiers In scenarios where a schema has multiple fields or nested structures, the default error messages can become overly complex and verbose. To address this, you can enhance the clarity and brevity of these messages by utilizing annotations such as `identifier`, `title`, and `description`. **Example** (Using Identifiers for Clarity) ```ts twoslash import { Schema } from "effect" const Name = Schema.String.annotations({ identifier: "Name" }) const Age = Schema.Number.annotations({ identifier: "Age" }) const Person = Schema.Struct({ name: Name, age: Age }).annotations({ identifier: "Person" }) Schema.decodeUnknownSync(Person)(null) /* throws: ParseError: Expected Person, actual null */ Schema.decodeUnknownSync(Person)({}, { errors: "all" }) /* throws: ParseError: Person ├─ ["name"] │ └─ is missing └─ ["age"] └─ is missing */ Schema.decodeUnknownSync(Person)( { name: null, age: null }, { errors: "all" } ) /* throws: ParseError: Person ├─ ["name"] │ └─ Expected Name, actual null └─ ["age"] └─ Expected Age, actual null */ ``` ### Refinements When a refinement fails, the default error message indicates whether the failure occurred in the "from" part or within the predicate defining the refinement: **Example** (Refinement Errors) ```ts twoslash import { Schema } from "effect" const Name = Schema.NonEmptyString.annotations({ identifier: "Name" }) const Age = Schema.Positive.pipe(Schema.int({ identifier: "Age" })) const Person = Schema.Struct({ name: Name, age: Age }).annotations({ identifier: "Person" }) // From side failure Schema.decodeUnknownSync(Person)({ name: null, age: 18 }) /* throws: ParseError: Person └─ ["name"] └─ Name └─ From side refinement failure └─ Expected string, actual null */ // Predicate refinement failure Schema.decodeUnknownSync(Person)({ name: "", age: 18 }) /* throws: ParseError: Person └─ ["name"] └─ Name └─ Predicate refinement failure └─ Expected a non empty string, actual "" */ ``` In the first example, the error message indicates a "from side" refinement failure in the `name` property, specifying that a string was expected but received `null`. In the second example, a "predicate" refinement failure is reported, indicating that a non-empty string was expected for `name` but an empty string was provided. ### Transformations Transformations between different types or formats can occasionally result in errors. The system provides a structured error message to specify where the error occurred: - **Encoded Side Failure:** Errors on this side typically indicate that the input to the transformation does not match the expected initial type or format. For example, receiving a `null` when a `string` is expected. - **Transformation Process Failure:** This type of error arises when the transformation logic itself fails, such as when the input does not meet the criteria specified within the transformation functions. - **Type Side Failure:** Occurs when the output of a transformation does not meet the schema requirements on the decoded side. This can happen if the transformed value fails subsequent validations or conditions. **Example** (Transformation Errors) ```ts twoslash import { ParseResult, Schema } from "effect" const schema = Schema.transformOrFail( Schema.String, Schema.String.pipe(Schema.minLength(2)), { strict: true, decode: (s, _, ast) => s.length > 0 ? ParseResult.succeed(s) : ParseResult.fail(new ParseResult.Type(ast, s)), encode: ParseResult.succeed } ) // Encoded side failure Schema.decodeUnknownSync(schema)(null) /* throws: ParseError: (string <-> minLength(2)) └─ Encoded side transformation failure └─ Expected string, actual null */ // transformation failure Schema.decodeUnknownSync(schema)("") /* throws: ParseError: (string <-> minLength(2)) └─ Transformation process failure └─ Expected (string <-> minLength(2)), actual "" */ // Type side failure Schema.decodeUnknownSync(schema)("a") /* throws: ParseError: (string <-> minLength(2)) └─ Type side transformation failure └─ minLength(2) └─ Predicate refinement failure └─ Expected a string at least 2 character(s) long, actual "a" */ ``` ## Custom Error Messages You have the capability to define custom error messages specifically tailored for different parts of your schema using the `message` annotation. This allows developers to provide more context-specific feedback which can improve the debugging and validation processes. Here's an overview of the `MessageAnnotation` type, which you can use to craft these messages: ```ts showLineNumbers=false type MessageAnnotation = (issue: ParseIssue) => | string | Effect | { readonly message: string | Effect readonly override: boolean } ``` | Return Type | Description | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `string` | Provides a static message that directly describes the error. | | `Effect` | Utilizes dynamic messages that can incorporate results from **synchronous** processes or rely on **optional** dependencies. | | Object (with `message` and `override`) | Allows you to define a specific error message along with a boolean flag (`override`). This flag determines if the custom message should supersede any default or nested custom messages, providing precise control over the error output displayed to users. | **Example** (Adding a Custom Error Message to a String Schema) ```ts twoslash import { Schema } from "effect" // Define a string schema without a custom message const MyString = Schema.String // Attempt to decode `null`, resulting in a default error message Schema.decodeUnknownSync(MyString)(null) /* throws: ParseError: Expected string, actual null */ // Define a string schema with a custom error message const MyStringWithMessage = Schema.String.annotations({ message: () => "not a string" }) // Decode with the custom schema, showing the new error message Schema.decodeUnknownSync(MyStringWithMessage)(null) /* throws: ParseError: not a string */ ``` **Example** (Custom Error Message for a Union Schema with Override Option) ```ts twoslash "override: true" import { Schema } from "effect" // Define a union schema without a custom message const MyUnion = Schema.Union(Schema.String, Schema.Number) // Decode `null`, resulting in default union error messages Schema.decodeUnknownSync(MyUnion)(null) /* throws: ParseError: string | number ├─ Expected string, actual null └─ Expected number, actual null */ // Define a union schema with a custom message and override flag const MyUnionWithMessage = Schema.Union( Schema.String, Schema.Number ).annotations({ message: () => ({ message: "Please provide a string or a number", // Ensures this message replaces all nested messages override: true }) }) // Decode with the custom schema, showing the new error message Schema.decodeUnknownSync(MyUnionWithMessage)(null) /* throws: ParseError: Please provide a string or a number */ ``` ### General Guidelines for Messages The general logic followed to determine the messages is as follows: 1. If no custom messages are set, the default message related to the innermost schema where the operation (i.e., decoding or encoding) failed is used. 2. If custom messages are set, then the message corresponding to the **first** failed schema is used, starting from the innermost schema to the outermost. However, if the failing schema does not have a custom message, then **the default message is used**. 3. As an opt-in feature, **you can override guideline 2** by setting the `overwrite` flag to `true`. This allows the custom message to take precedence over all other custom messages from inner schemas. This is to address the scenario where a user wants to define a single cumulative custom message describing the properties that a valid value must have and does not want to see default messages. Let's see some practical examples. ### Scalar Schemas **Example** (Simple Custom Message for Scalar Schema) ```ts twoslash import { Schema } from "effect" const MyString = Schema.String.annotations({ message: () => "my custom message" }) const decode = Schema.decodeUnknownSync(MyString) try { decode(null) } catch (e: any) { console.log(e.message) // "my custom message" } ``` ### Refinements This example demonstrates setting a custom message on the last refinement in a chain of refinements. As you can see, the custom message is only used if the refinement related to `maxLength` fails; otherwise, default messages are used. **Example** (Custom Message on Last Refinement in Chain) ```ts twoslash import { Schema } from "effect" const MyString = Schema.String.pipe( Schema.minLength(1), Schema.maxLength(2) ).annotations({ // This message is displayed only if the last filter (`maxLength`) fails message: () => "my custom message" }) const decode = Schema.decodeUnknownSync(MyString) try { decode(null) } catch (e: any) { console.log(e.message) /* minLength(1) & maxLength(2) └─ From side refinement failure └─ minLength(1) └─ From side refinement failure └─ Expected string, actual null */ } try { decode("") } catch (e: any) { console.log(e.message) /* minLength(1) & maxLength(2) └─ From side refinement failure └─ minLength(1) └─ Predicate refinement failure └─ Expected a string at least 1 character(s) long, actual "" */ } try { decode("abc") } catch (e: any) { console.log(e.message) // "my custom message" } ``` When setting multiple custom messages, the one corresponding to the **first** failed predicate is used, starting from the innermost refinement to the outermost: **Example** (Custom Messages for Multiple Refinements) ```ts twoslash import { Schema } from "effect" const MyString = Schema.String // This message is displayed only if a non-String is passed as input .annotations({ message: () => "String custom message" }) .pipe( // This message is displayed only if the filter `minLength` fails Schema.minLength(1, { message: () => "minLength custom message" }), // This message is displayed only if the filter `maxLength` fails Schema.maxLength(2, { message: () => "maxLength custom message" }) ) const decode = Schema.decodeUnknownSync(MyString) try { decode(null) } catch (e: any) { console.log(e.message) // String custom message } try { decode("") } catch (e: any) { console.log(e.message) // minLength custom message } try { decode("abc") } catch (e: any) { console.log(e.message) // maxLength custom message } ``` You have the option to change the default behavior by setting the `override` flag to `true`. This is useful when you want to create a single comprehensive custom message that describes the required properties of a valid value without displaying default messages. **Example** (Overriding Default Messages) ```ts twoslash import { Schema } from "effect" const MyString = Schema.String.pipe( Schema.minLength(1), Schema.maxLength(2) ).annotations({ // By setting the `override` flag to `true`, this message will always be shown for any error message: () => ({ message: "my custom message", override: true }) }) const decode = Schema.decodeUnknownSync(MyString) try { decode(null) } catch (e: any) { console.log(e.message) // my custom message } try { decode("") } catch (e: any) { console.log(e.message) // my custom message } try { decode("abc") } catch (e: any) { console.log(e.message) // my custom message } ``` ### Transformations In this example, `IntFromString` is a transformation schema that converts strings to integers. It applies specific validation messages based on different scenarios. **Example** (Custom Error Messages for String-to-Integer Transformation) ```ts twoslash import { ParseResult, Schema } from "effect" const IntFromString = Schema.transformOrFail( // This message is displayed only if the input is not a string Schema.String.annotations({ message: () => "please enter a string" }), // This message is displayed only if the input can be converted // to a number but it's not an integer Schema.Int.annotations({ message: () => "please enter an integer" }), { strict: true, decode: (s, _, ast) => { const n = Number(s) return Number.isNaN(n) ? ParseResult.fail(new ParseResult.Type(ast, s)) : ParseResult.succeed(n) }, encode: (n) => ParseResult.succeed(String(n)) } ) // This message is displayed only if the input // cannot be converted to a number .annotations({ message: () => "please enter a parseable string" }) const decode = Schema.decodeUnknownSync(IntFromString) try { decode(null) } catch (e: any) { console.log(e.message) // please enter a string } try { decode("1.2") } catch (e: any) { console.log(e.message) // please enter an integer } try { decode("not a number") } catch (e: any) { console.log(e.message) // please enter a parseable string } ``` ### Compound Schemas The custom message system becomes especially handy when dealing with complex schemas, unlike simple scalar values like `string` or `number`. For instance, consider a schema comprising nested structures, such as a struct containing an array of other structs. Let's explore an example demonstrating the advantage of default messages in handling decoding errors within such nested structures: **Example** (Custom Error Messages in Nested Schemas) ```ts twoslash import { Schema, pipe } from "effect" const schema = Schema.Struct({ outcomes: pipe( Schema.Array( Schema.Struct({ id: Schema.String, text: pipe( Schema.String.annotations({ message: () => "error_invalid_outcome_type" }), Schema.minLength(1, { message: () => "error_required_field" }), Schema.maxLength(50, { message: () => "error_max_length_field" }) ) }) ), Schema.minItems(1, { message: () => "error_min_length_field" }) ) }) Schema.decodeUnknownSync(schema, { errors: "all" })({ outcomes: [] }) /* throws ParseError: { readonly outcomes: minItems(1) } └─ ["outcomes"] └─ error_min_length_field */ Schema.decodeUnknownSync(schema, { errors: "all" })({ outcomes: [ { id: "1", text: "" }, { id: "2", text: "this one is valid" }, { id: "3", text: "1234567890".repeat(6) } ] }) /* throws ParseError: { readonly outcomes: minItems(1) } └─ ["outcomes"] └─ minItems(1) └─ From side refinement failure └─ ReadonlyArray<{ readonly id: string; readonly text: minLength(1) & maxLength(50) }> ├─ [0] │ └─ { readonly id: string; readonly text: minLength(1) & maxLength(50) } │ └─ ["text"] │ └─ error_required_field └─ [2] └─ { readonly id: string; readonly text: minLength(1) & maxLength(50) } └─ ["text"] └─ error_max_length_field */ ``` ### Effectful messages Error messages can go beyond simple strings by returning an `Effect`, allowing them to access dependencies, such as an internationalization service. This approach lets messages dynamically adjust based on external context or services. Below is an example illustrating how to create effect-based messages. **Example** (Effect-Based Message with Internationalization Service) ```ts twoslash import { Context, Effect, Either, Option, Schema, ParseResult } from "effect" // Define an internationalization service for custom messages class Messages extends Context.Tag("Messages")< Messages, { NonEmpty: string } >() {} // Define a schema with an effect-based message // that depends on the Messages service const Name = Schema.NonEmptyString.annotations({ message: () => Effect.gen(function* () { // Attempt to retrieve the Messages service const service = yield* Effect.serviceOption(Messages) // Use a fallback message if the service is not available return Option.match(service, { onNone: () => "Invalid string", onSome: (messages) => messages.NonEmpty }) }) }) // Attempt to decode an empty string without providing the Messages service Schema.decodeUnknownEither(Name)("").pipe( Either.mapLeft((error) => ParseResult.TreeFormatter.formatError(error).pipe( Effect.runSync, console.log ) ) ) // Output: Invalid string // Provide the Messages service to customize the error message Schema.decodeUnknownEither(Name)("").pipe( Either.mapLeft((error) => ParseResult.TreeFormatter.formatError(error).pipe( Effect.provideService(Messages, { NonEmpty: "should be non empty" }), Effect.runSync, console.log ) ) ) // Output: should be non empty ``` ### Missing messages You can provide custom messages for missing fields or tuple elements using the `missingMessage` annotation. **Example** (Custom Message for Missing Property) In this example, a custom message is defined for a missing `name` property in the `Person` schema. ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.propertySignature(Schema.String).annotations({ // Custom message if "name" is missing missingMessage: () => "Name is required" }) }) Schema.decodeUnknownSync(Person)({}) /* throws: ParseError: { readonly name: string } └─ ["name"] └─ Name is required */ ``` **Example** (Custom Message for Missing Tuple Elements) Here, each element in the `Point` tuple schema has a specific custom message if the element is missing. ```ts twoslash import { Schema } from "effect" const Point = Schema.Tuple( Schema.element(Schema.Number).annotations({ // Message if X is missing missingMessage: () => "X coordinate is required" }), Schema.element(Schema.Number).annotations({ // Message if Y is missing missingMessage: () => "Y coordinate is required" }) ) Schema.decodeUnknownSync(Point)([], { errors: "all" }) /* throws: ParseError: readonly [number, number] ├─ [0] │ └─ X coordinate is required └─ [1] └─ Y coordinate is required */ ``` # [Filters](https://effect.website/docs/schema/filters/) ## Overview import { Aside } from "@astrojs/starlight/components" Developers can define custom validation logic beyond basic type checks, giving more control over how data is validated. ## Declaring Filters Filters are declared using the `Schema.filter` function. This function requires two arguments: the schema to be validated and a predicate function. The predicate function is user-defined and determines whether the data satisfies the condition. If the data fails the validation, an error message can be provided. **Example** (Defining a Minimum String Length Filter) ```ts twoslash import { Schema } from "effect" // Define a string schema with a filter to ensure the string // is at least 10 characters long const LongString = Schema.String.pipe( Schema.filter( // Custom error message for strings shorter than 10 characters (s) => s.length >= 10 || "a string at least 10 characters long" ) ) // ┌─── string // ▼ type Type = typeof LongString.Type console.log(Schema.decodeUnknownSync(LongString)("a")) /* throws: ParseError: { string | filter } └─ Predicate refinement failure └─ a string at least 10 characters long */ ``` Note that the filter does not alter the schema's `Type`: ```ts showLineNumbers=false // ┌─── string // ▼ type Type = typeof LongString.Type ``` Filters add additional validation constraints without modifying the schema's underlying type. ## The Predicate Function The predicate function in a filter follows this structure: ```ts type Predicate = ( a: A, options: ParseOptions, self: AST.Refinement ) => FilterReturnType ``` where ```ts interface FilterIssue { readonly path: ReadonlyArray readonly issue: string | ParseResult.ParseIssue } type FilterOutput = | undefined | boolean | string | ParseResult.ParseIssue | FilterIssue type FilterReturnType = FilterOutput | ReadonlyArray ``` The filter's predicate can return several types of values, each affecting validation in a different way: | Return Type | Behavior | | ----------------------------- | ------------------------------------------------------------------------------------------------ | | `true` | The data satisfies the filter's condition and passes validation. | | `false` or `undefined` | The data does not meet the condition, and no specific error message is provided. | | `string` | The validation fails, and the provided string is used as the error message. | | `ParseResult.ParseIssue` | The validation fails with a detailed error structure, specifying where and why it failed. | | `FilterIssue` | Allows for more detailed error messages with specific paths, providing enhanced error reporting. | | `ReadonlyArray` | An array of issues can be returned if multiple validation errors need to be reported. | ## Adding Annotations Embedding metadata within the schema, such as identifiers, JSON schema specifications, and descriptions, enhances understanding and analysis of the schema's constraints and purpose. **Example** (Adding Metadata with Annotations) ```ts twoslash import { Schema, JSONSchema } from "effect" const LongString = Schema.String.pipe( Schema.filter( (s) => s.length >= 10 ? undefined : "a string at least 10 characters long", { identifier: "LongString", jsonSchema: { minLength: 10 }, description: "Lorem ipsum dolor sit amet, ..." } ) ) console.log(Schema.decodeUnknownSync(LongString)("a")) /* throws: ParseError: LongString └─ Predicate refinement failure └─ a string at least 10 characters long */ console.log(JSON.stringify(JSONSchema.make(LongString), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "$defs": { "LongString": { "type": "string", "description": "Lorem ipsum dolor sit amet, ...", "minLength": 10 } }, "$ref": "#/$defs/LongString" } */ ``` ## Specifying Error Paths When validating forms or structured data, it's possible to associate specific error messages with particular fields or paths. This enhances error reporting and is especially useful when integrating with libraries like [react-hook-form](https://react-hook-form.com/). **Example** (Matching Passwords) ```ts twoslash import { Either, Schema, ParseResult } from "effect" const Password = Schema.Trim.pipe(Schema.minLength(2)) const MyForm = Schema.Struct({ password: Password, confirm_password: Password }).pipe( // Add a filter to ensure that passwords match Schema.filter((input) => { if (input.password !== input.confirm_password) { // Return an error message associated // with the "confirm_password" field return { path: ["confirm_password"], message: "Passwords do not match" } } }) ) console.log( JSON.stringify( Schema.decodeUnknownEither(MyForm)({ password: "abc", confirm_password: "abd" // Confirm password does not match }).pipe( Either.mapLeft((error) => ParseResult.ArrayFormatter.formatErrorSync(error) ) ), null, 2 ) ) /* "_id": "Either", "_tag": "Left", "left": [ { "_tag": "Type", "path": [ "confirm_password" ], "message": "Passwords do not match" } ] } */ ``` In this example, we define a `MyForm` schema with two password fields (`password` and `confirm_password`). We use `Schema.filter` to check that both passwords match. If they don't, an error message is returned, specifically associated with the `confirm_password` field. This makes it easier to pinpoint the exact location of the validation failure. The error is formatted in a structured way using `ArrayFormatter`, allowing for easier post-processing and integration with form libraries. ## Multiple Error Reporting The `Schema.filter` API supports reporting multiple validation issues at once, which is especially useful in scenarios like form validation where several checks might fail simultaneously. **Example** (Reporting Multiple Validation Errors) ```ts twoslash import { Either, Schema, ParseResult } from "effect" const Password = Schema.Trim.pipe(Schema.minLength(2)) const OptionalString = Schema.optional(Schema.String) const MyForm = Schema.Struct({ password: Password, confirm_password: Password, name: OptionalString, surname: OptionalString }).pipe( Schema.filter((input) => { const issues: Array = [] // Check if passwords match if (input.password !== input.confirm_password) { issues.push({ path: ["confirm_password"], message: "Passwords do not match" }) } // Ensure either name or surname is present if (!input.name && !input.surname) { issues.push({ path: ["surname"], message: "Surname must be present if name is not present" }) } return issues }) ) console.log( JSON.stringify( Schema.decodeUnknownEither(MyForm)({ password: "abc", confirm_password: "abd" // Confirm password does not match }).pipe( Either.mapLeft((error) => ParseResult.ArrayFormatter.formatErrorSync(error) ) ), null, 2 ) ) /* { "_id": "Either", "_tag": "Left", "left": [ { "_tag": "Type", "path": [ "confirm_password" ], "message": "Passwords do not match" }, { "_tag": "Type", "path": [ "surname" ], "message": "Surname must be present if name is not present" } ] } */ ``` In this example, we define a `MyForm` schema with fields for password validation and optional name/surname fields. The `Schema.filter` function checks if the passwords match and ensures that either a name or surname is provided. If either validation fails, the corresponding error message is associated with the relevant field and both errors are returned in a structured format. ## Exposed Values For schemas with filters, you can access the base schema (the schema before the filter was applied) using the `from` property: ```ts twoslash import { Schema } from "effect" const LongString = Schema.String.pipe( Schema.filter((s) => s.length >= 10) ) // Access the base schema, which is the string schema // before the filter was applied // // ┌─── typeof Schema.String // ▼ const From = LongString.from ``` ## Built-in Filters ### String Filters Here is a list of useful string filters provided by the Schema module: ```ts twoslash import { Schema } from "effect" // Specifies maximum length of a string Schema.String.pipe(Schema.maxLength(5)) // Specifies minimum length of a string Schema.String.pipe(Schema.minLength(5)) // Equivalent to minLength(1) Schema.String.pipe(Schema.nonEmptyString()) // or Schema.NonEmptyString // Specifies exact length of a string Schema.String.pipe(Schema.length(5)) // Specifies a range for the length of a string Schema.String.pipe(Schema.length({ min: 2, max: 4 })) // Matches a string against a regular expression pattern Schema.String.pipe(Schema.pattern(/^[a-z]+$/)) // Ensures a string starts with a specific substring Schema.String.pipe(Schema.startsWith("prefix")) // Ensures a string ends with a specific substring Schema.String.pipe(Schema.endsWith("suffix")) // Checks if a string includes a specific substring Schema.String.pipe(Schema.includes("substring")) // Validates that a string has no leading or trailing whitespaces Schema.String.pipe(Schema.trimmed()) // Validates that a string is entirely in lowercase Schema.String.pipe(Schema.lowercased()) // Validates that a string is entirely in uppercase Schema.String.pipe(Schema.uppercased()) // Validates that a string is capitalized Schema.String.pipe(Schema.capitalized()) // Validates that a string is uncapitalized Schema.String.pipe(Schema.uncapitalized()) ``` ### Number Filters Here is a list of useful number filters provided by the Schema module: ```ts twoslash import { Schema } from "effect" // Specifies a number greater than 5 Schema.Number.pipe(Schema.greaterThan(5)) // Specifies a number greater than or equal to 5 Schema.Number.pipe(Schema.greaterThanOrEqualTo(5)) // Specifies a number less than 5 Schema.Number.pipe(Schema.lessThan(5)) // Specifies a number less than or equal to 5 Schema.Number.pipe(Schema.lessThanOrEqualTo(5)) // Specifies a number between -2 and 2, inclusive Schema.Number.pipe(Schema.between(-2, 2)) // Specifies that the value must be an integer Schema.Number.pipe(Schema.int()) // or Schema.Int // Ensures the value is not NaN Schema.Number.pipe(Schema.nonNaN()) // or Schema.NonNaN // Ensures that the provided value is a finite number // (excluding NaN, +Infinity, and -Infinity) Schema.Number.pipe(Schema.finite()) // or Schema.Finite // Specifies a positive number (> 0) Schema.Number.pipe(Schema.positive()) // or Schema.Positive // Specifies a non-negative number (>= 0) Schema.Number.pipe(Schema.nonNegative()) // or Schema.NonNegative // A non-negative integer Schema.NonNegativeInt // Specifies a negative number (< 0) Schema.Number.pipe(Schema.negative()) // or Schema.Negative // Specifies a non-positive number (<= 0) Schema.Number.pipe(Schema.nonPositive()) // or Schema.NonPositive // Specifies a number that is evenly divisible by 5 Schema.Number.pipe(Schema.multipleOf(5)) // A 8-bit unsigned integer (0 to 255) Schema.Uint8 ``` ### ReadonlyArray Filters Here is a list of useful array filters provided by the Schema module: ```ts twoslash import { Schema } from "effect" // Specifies the maximum number of items in the array Schema.Array(Schema.Number).pipe(Schema.maxItems(2)) // Specifies the minimum number of items in the array Schema.Array(Schema.Number).pipe(Schema.minItems(2)) // Specifies the exact number of items in the array Schema.Array(Schema.Number).pipe(Schema.itemsCount(2)) ``` ### Date Filters ```ts twoslash import { Schema } from "effect" // Specifies a valid date (rejects values like `new Date("Invalid Date")`) Schema.DateFromSelf.pipe(Schema.validDate()) // or Schema.ValidDateFromSelf // Specifies a date greater than the current date Schema.Date.pipe(Schema.greaterThanDate(new Date())) // Specifies a date greater than or equal to the current date Schema.Date.pipe(Schema.greaterThanOrEqualToDate(new Date())) // Specifies a date less than the current date Schema.Date.pipe(Schema.lessThanDate(new Date())) // Specifies a date less than or equal to the current date Schema.Date.pipe(Schema.lessThanOrEqualToDate(new Date())) // Specifies a date between two dates Schema.Date.pipe(Schema.betweenDate(new Date(0), new Date())) ``` ### BigInt Filters Here is a list of useful `BigInt` filters provided by the Schema module: ```ts twoslash import { Schema } from "effect" // Specifies a BigInt greater than 5 Schema.BigInt.pipe(Schema.greaterThanBigInt(5n)) // Specifies a BigInt greater than or equal to 5 Schema.BigInt.pipe(Schema.greaterThanOrEqualToBigInt(5n)) // Specifies a BigInt less than 5 Schema.BigInt.pipe(Schema.lessThanBigInt(5n)) // Specifies a BigInt less than or equal to 5 Schema.BigInt.pipe(Schema.lessThanOrEqualToBigInt(5n)) // Specifies a BigInt between -2n and 2n, inclusive Schema.BigInt.pipe(Schema.betweenBigInt(-2n, 2n)) // Specifies a positive BigInt (> 0n) Schema.BigInt.pipe(Schema.positiveBigInt()) // or Schema.PositiveBigIntFromSelf // Specifies a non-negative BigInt (>= 0n) Schema.BigInt.pipe(Schema.nonNegativeBigInt()) // or Schema.NonNegativeBigIntFromSelf // Specifies a negative BigInt (< 0n) Schema.BigInt.pipe(Schema.negativeBigInt()) // or Schema.NegativeBigIntFromSelf // Specifies a non-positive BigInt (<= 0n) Schema.BigInt.pipe(Schema.nonPositiveBigInt()) // or Schema.NonPositiveBigIntFromSelf ``` ### BigDecimal Filters Here is a list of useful `BigDecimal` filters provided by the Schema module: ```ts twoslash import { Schema, BigDecimal } from "effect" // Specifies a BigDecimal greater than 5 Schema.BigDecimal.pipe( Schema.greaterThanBigDecimal(BigDecimal.unsafeFromNumber(5)) ) // Specifies a BigDecimal greater than or equal to 5 Schema.BigDecimal.pipe( Schema.greaterThanOrEqualToBigDecimal(BigDecimal.unsafeFromNumber(5)) ) // Specifies a BigDecimal less than 5 Schema.BigDecimal.pipe( Schema.lessThanBigDecimal(BigDecimal.unsafeFromNumber(5)) ) // Specifies a BigDecimal less than or equal to 5 Schema.BigDecimal.pipe( Schema.lessThanOrEqualToBigDecimal(BigDecimal.unsafeFromNumber(5)) ) // Specifies a BigDecimal between -2 and 2, inclusive Schema.BigDecimal.pipe( Schema.betweenBigDecimal( BigDecimal.unsafeFromNumber(-2), BigDecimal.unsafeFromNumber(2) ) ) // Specifies a positive BigDecimal (> 0) Schema.BigDecimal.pipe(Schema.positiveBigDecimal()) // Specifies a non-negative BigDecimal (>= 0) Schema.BigDecimal.pipe(Schema.nonNegativeBigDecimal()) // Specifies a negative BigDecimal (< 0) Schema.BigDecimal.pipe(Schema.negativeBigDecimal()) // Specifies a non-positive BigDecimal (<= 0) Schema.BigDecimal.pipe(Schema.nonPositiveBigDecimal()) ``` ### Duration Filters Here is a list of useful [Duration](/docs/data-types/duration/) filters provided by the Schema module: ```ts twoslash import { Schema } from "effect" // Specifies a duration greater than 5 seconds Schema.Duration.pipe(Schema.greaterThanDuration("5 seconds")) // Specifies a duration greater than or equal to 5 seconds Schema.Duration.pipe(Schema.greaterThanOrEqualToDuration("5 seconds")) // Specifies a duration less than 5 seconds Schema.Duration.pipe(Schema.lessThanDuration("5 seconds")) // Specifies a duration less than or equal to 5 seconds Schema.Duration.pipe(Schema.lessThanOrEqualToDuration("5 seconds")) // Specifies a duration between 5 seconds and 10 seconds, inclusive Schema.Duration.pipe(Schema.betweenDuration("5 seconds", "10 seconds")) ``` # [Getting Started](https://effect.website/docs/schema/getting-started/) ## Overview import { Aside } from "@astrojs/starlight/components" You can import the necessary types and functions from the `effect/Schema` module: **Example** (Namespace Import) ```ts showLineNumbers=false import * as Schema from "effect/Schema" ``` **Example** (Named Import) ```ts showLineNumbers=false import { Schema } from "effect" ``` ## Defining a schema One common way to define a `Schema` is by utilizing the `Struct` constructor. This constructor allows you to create a new schema that outlines an object with specific properties. Each property in the object is defined by its own schema, which specifies the data type and any validation rules. **Example** (Defining a Simple Object Schema) This `Person` schema describes an object with a `name` (string) and `age` (number) property: ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) ``` ## Extracting Inferred Types ### Type Once you've defined a schema (`Schema`), you can extract the inferred type `Type` in two ways: 1. Using the `Schema.Type` utility 2. Accessing the `Type` field directly on your schema **Example** (Extracting Inferred Type) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) // 1. Using the Schema.Type utility type Person = Schema.Schema.Type // 2. Accessing the Type field directly type Person2 = typeof Person.Type ``` The resulting type will look like this: ```ts showLineNumbers=false type Person = { readonly name: string readonly age: number } ``` Alternatively, you can extract the `Person` type using the `interface` keyword, which may improve readability and performance in some cases. **Example** (Extracting Type with an Interface) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) interface Person extends Schema.Schema.Type {} ``` Both approaches yield the same result, but using an interface provides benefits such as performance advantages and improved readability. ### Encoded In a `Schema`, the `Encoded` type can differ from the `Type` type, representing the format in which data is encoded. You can extract the `Encoded` type in two ways: 1. Using the `Schema.Encoded` utility 2. Accessing the `Encoded` field directly on the schema **Example** (Extracting the Encoded Type) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, // a schema that decodes a string to a number age: Schema.NumberFromString }) // 1. Using the Schema.Encoded utility type PersonEncoded = Schema.Schema.Encoded // 2. Accessing the Encoded field directly type PersonEncoded2 = typeof Person.Encoded ``` The resulting type is: ```ts showLineNumbers=false type PersonEncoded = { readonly name: string readonly age: string } ``` Note that `age` is of type `string` in the `Encoded` type of the schema and is of type `number` in the `Type` type of the schema. Alternatively, you can define the `PersonEncoded` type using the `interface` keyword, which can enhance readability and performance. **Example** (Extracting Encoded Type with an Interface) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, // a schema that decodes a string to a number age: Schema.NumberFromString }) interface PersonEncoded extends Schema.Schema.Encoded {} ``` Both approaches yield the same result, but using an interface provides benefits such as performance advantages and improved readability. ### Context In a `Schema`, the `Context` type represents any external data or dependencies that the schema requires to perform encoding or decoding. You can extract the inferred `Context` type in two ways: 1. Using the `Schema.Context` utility. 2. Accessing the `Context` field on the schema. **Example** (Extracting the Context Type) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) // 1. Using the Schema.Context utility type PersonContext = Schema.Schema.Context // 2. Accessing the Context field directly type PersonContext2 = typeof Person.Context ``` ### Schemas with Opaque Types When defining a schema, you may want to create a schema with an opaque type. This is useful when you want to hide the internal structure of the schema and only expose the type of the schema. **Example** (Creating an Opaque Schema) To create a schema with an opaque type, you can use the following technique that re-declares the schema: ```ts twoslash import { Schema } from "effect" // Define the schema structure const _Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) // Declare the type interface to make it opaque interface Person extends Schema.Schema.Type {} // Re-declare the schema as opaque const Person: Schema.Schema = _Person ``` Alternatively, you can use the Class APIs (see the [Class APIs](/docs/schema/classes/) section for more details). Note that the technique shown above becomes more complex when the schema is defined such that `Type` is different from `Encoded`. **Example** (Opaque Schema with Different Type and Encoded) ```ts twoslash "NumberFromString" import { Schema } from "effect" // Define the schema structure, with a field that // decodes a string to a number const _Person = Schema.Struct({ name: Schema.String, age: Schema.NumberFromString }) // Create the `Type` interface for an opaque schema interface Person extends Schema.Schema.Type {} // Create the `Encoded` interface for an opaque schema interface PersonEncoded extends Schema.Schema.Encoded {} // Re-declare the schema with opaque Type and Encoded const Person: Schema.Schema = _Person ``` In this case, the field `"age"` is of type `string` in the `Encoded` type of the schema and is of type `number` in the `Type` type of the schema. Therefore, we need to define **two** interfaces (`PersonEncoded` and `Person`) and use both to redeclare our final schema `Person`. ## Readonly Types by Default It's important to note that by default, most constructors exported by `effect/Schema` return `readonly` types. **Example** (Readonly Types in a Schema) For instance, in the `Person` schema below: ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) ``` the resulting inferred `Type` would be: ```ts showLineNumbers=false "readonly" { readonly name: string; readonly age: number; } ``` ## Decoding When working with unknown data types in TypeScript, decoding them into a known structure can be challenging. Luckily, `effect/Schema` provides several functions to help with this process. Let's explore how to decode unknown values using these functions. | API | Description | | ---------------------- | -------------------------------------------------------------------------------- | | `decodeUnknownSync` | Synchronously decodes a value and throws an error if parsing fails. | | `decodeUnknownOption` | Decodes a value and returns an [Option](/docs/data-types/option/) type. | | `decodeUnknownEither` | Decodes a value and returns an [Either](/docs/data-types/either/) type. | | `decodeUnknownPromise` | Decodes a value and returns a `Promise`. | | `decodeUnknown` | Decodes a value and returns an [Effect](/docs/getting-started/the-effect-type/). | ### decodeUnknownSync The `Schema.decodeUnknownSync` function is useful when you want to parse a value and immediately throw an error if the parsing fails. **Example** (Using `decodeUnknownSync` for Immediate Decoding) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) // Simulate an unknown input const input: unknown = { name: "Alice", age: 30 } // Example of valid input matching the schema console.log(Schema.decodeUnknownSync(Person)(input)) // Output: { name: 'Alice', age: 30 } // Example of invalid input that does not match the schema console.log(Schema.decodeUnknownSync(Person)(null)) /* throws: ParseError: Expected { readonly name: string; readonly age: number }, actual null */ ``` ### decodeUnknownEither The `Schema.decodeUnknownEither` function allows you to parse a value and receive the result as an [Either](/docs/data-types/either/), representing success (`Right`) or failure (`Left`). This approach lets you handle parsing errors more gracefully without throwing exceptions. **Example** (Using `Schema.decodeUnknownEither` for Error Handling) ```ts twoslash import { Schema } from "effect" import { Either } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) const decode = Schema.decodeUnknownEither(Person) // Simulate an unknown input const input: unknown = { name: "Alice", age: 30 } // Attempt decoding a valid input const result1 = decode(input) if (Either.isRight(result1)) { console.log(result1.right) /* Output: { name: "Alice", age: 30 } */ } // Simulate decoding an invalid input const result2 = decode(null) if (Either.isLeft(result2)) { console.log(result2.left) /* Output: { _id: 'ParseError', message: 'Expected { readonly name: string; readonly age: number }, actual null' } */ } ``` ### decodeUnknown If your schema involves asynchronous transformations, the `Schema.decodeUnknownSync` and `Schema.decodeUnknownEither` functions will not be suitable. In such cases, you should use the `Schema.decodeUnknown` function, which returns an [Effect](/docs/getting-started/the-effect-type/). **Example** (Handling Asynchronous Decoding) ```ts twoslash import { Schema } from "effect" import { Effect } from "effect" const PersonId = Schema.Number const Person = Schema.Struct({ id: PersonId, name: Schema.String, age: Schema.Number }) const asyncSchema = Schema.transformOrFail(PersonId, Person, { strict: true, // Decode with simulated async transformation decode: (id) => Effect.succeed({ id, name: "name", age: 18 }).pipe( Effect.delay("10 millis") ), encode: (person) => Effect.succeed(person.id).pipe(Effect.delay("10 millis")) }) // Attempting to use a synchronous decoder on an async schema console.log(Schema.decodeUnknownEither(asyncSchema)(1)) /* Output: { _id: 'Either', _tag: 'Left', left: { _id: 'ParseError', message: '(number <-> { readonly id: number; readonly name: string; readonly age: number })\n' + '└─ cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work' } } */ // Decoding asynchronously with `Schema.decodeUnknown` Effect.runPromise(Schema.decodeUnknown(asyncSchema)(1)).then(console.log) /* Output: { id: 1, name: 'name', age: 18 } */ ``` In the code above, the first approach using `Schema.decodeUnknownEither` results in an error indicating that the transformation cannot be resolved synchronously. This occurs because `Schema.decodeUnknownEither` is not designed for async operations. The second approach, which uses `Schema.decodeUnknown`, works correctly, allowing you to handle asynchronous transformations and return the expected result. ## Encoding The `Schema` module provides several `encode*` functions to encode data according to a schema: | API | Description | | --------------- | ---------------------------------------------------------------------------------------------------- | | `encodeSync` | Synchronously encodes data and throws an error if encoding fails. | | `encodeOption` | Encodes data and returns an [Option](/docs/data-types/option/) type. | | `encodeEither` | Encodes data and returns an [Either](/docs/data-types/either/) type representing success or failure. | | `encodePromise` | Encodes data and returns a `Promise`. | | `encode` | Encodes data and returns an [Effect](/docs/getting-started/the-effect-type/). | **Example** (Using `Schema.encodeSync` for Immediate Encoding) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ // Ensure name is a non-empty string name: Schema.NonEmptyString, // Allow age to be decoded from a string and encoded to a string age: Schema.NumberFromString }) // Valid input: encoding succeeds and returns expected types console.log(Schema.encodeSync(Person)({ name: "Alice", age: 30 })) // Output: { name: 'Alice', age: '30' } // Invalid input: encoding fails due to empty name string console.log(Schema.encodeSync(Person)({ name: "", age: 30 })) /* throws: ParseError: { readonly name: NonEmptyString; readonly age: NumberFromString } └─ ["name"] └─ NonEmptyString └─ Predicate refinement failure └─ Expected a non empty string, actual "" */ ``` Note that during encoding, the number value `30` was converted to a string `"30"`. ### Handling Unsupported Encoding In certain cases, it may not be feasible to support encoding for a schema. While it is generally advised to define schemas that allow both decoding and encoding, there are situations where encoding a particular type is either unsupported or unnecessary. In these instances, the `Forbidden` issue can signal that encoding is not available for certain values. **Example** (Using `Forbidden` to Indicate Unsupported Encoding) Here is an example of a transformation that never fails during decoding. It returns an [Either](/docs/data-types/either/) containing either the decoded value or the original input. For encoding, it is reasonable to not support it and use `Forbidden` as the result. ```ts twoslash import { Either, ParseResult, Schema } from "effect" // Define a schema that safely decodes to Either type export const SafeDecode = (self: Schema.Schema) => { const decodeUnknownEither = Schema.decodeUnknownEither(self) return Schema.transformOrFail( Schema.Unknown, Schema.EitherFromSelf({ left: Schema.Unknown, right: Schema.typeSchema(self) }), { strict: true, // Decode: map a failed result to the input as Left, // successful result as Right decode: (input) => ParseResult.succeed( Either.mapLeft(decodeUnknownEither(input), () => input) ), // Encode: only support encoding Right values, // Left values raise Forbidden error encode: (actual, _, ast) => Either.match(actual, { onLeft: () => ParseResult.fail( new ParseResult.Forbidden( ast, actual, "cannot encode a Left" ) ), // Successfully encode a Right value onRight: ParseResult.succeed }) } ) } ``` **Explanation** - **Decoding**: The `SafeDecode` function ensures that decoding never fails. It wraps the decoded value in an [Either](/docs/data-types/either/), where a successful decoding results in a `Right` and a failed decoding results in a `Left` containing the original input. - **Encoding**: The encoding process uses the `Forbidden` error to indicate that encoding a `Left` value is not supported. Only `Right` values are successfully encoded. ## ParseError The `Schema.decodeUnknownEither` and `Schema.encodeEither` functions returns a [Either](/docs/data-types/either/): ```ts showLineNumbers=false Either ``` where `ParseError` is defined as follows (simplified): ```ts showLineNumbers=false interface ParseError { readonly _tag: "ParseError" readonly issue: ParseIssue } ``` In this structure, `ParseIssue` represents an error that might occur during the parsing process. It is wrapped in a tagged error to make it easier to catch errors using [Effect.catchTag](/docs/error-management/expected-errors/#catchtag). The result `Either` contains the inferred data type described by the schema (`Type`). A successful parse yields a `Right` value with the parsed data `Type`, while a failed parse results in a `Left` value containing a `ParseError`. ## Parse Options The options below provide control over both decoding and encoding behaviors. ### Managing Excess properties By default, any properties not defined in the schema are removed from the output when parsing a value. This ensures the parsed data conforms strictly to the expected structure. If you want to detect and handle unexpected properties, use the `onExcessProperty` option (default value: `"ignore"`), which allows you to raise an error for excess properties. This can be helpful when you need to validate and catch unanticipated properties. **Example** (Setting `onExcessProperty` to `"error"`) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) // Excess properties are ignored by default console.log( Schema.decodeUnknownSync(Person)({ name: "Bob", age: 40, email: "bob@example.com" // Ignored }) ) /* Output: { name: 'Bob', age: 40 } */ // With `onExcessProperty` set to "error", // an error is thrown for excess properties Schema.decodeUnknownSync(Person)( { name: "Bob", age: 40, email: "bob@example.com" // Will raise an error }, { onExcessProperty: "error" } ) /* throws ParseError: { readonly name: string; readonly age: number } └─ ["email"] └─ is unexpected, expected: "name" | "age" */ ``` To retain extra properties, set `onExcessProperty` to `"preserve"`. **Example** (Setting `onExcessProperty` to `"preserve"`) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) // Excess properties are preserved in the output console.log( Schema.decodeUnknownSync(Person)( { name: "Bob", age: 40, email: "bob@example.com" }, { onExcessProperty: "preserve" } ) ) /* { email: 'bob@example.com', name: 'Bob', age: 40 } */ ``` ### Receive all errors The `errors` option enables you to retrieve all errors encountered during parsing. By default, only the first error is returned. Setting `errors` to `"all"` provides comprehensive error feedback, which can be useful for debugging or offering detailed validation feedback. **Example** (Setting `errors` to `"all"`) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) // Attempt to parse with multiple issues in the input data Schema.decodeUnknownSync(Person)( { name: "Bob", age: "abc", email: "bob@example.com" }, { errors: "all", onExcessProperty: "error" } ) /* throws ParseError: { readonly name: string; readonly age: number } ├─ ["email"] │ └─ is unexpected, expected: "name" | "age" └─ ["age"] └─ Expected number, actual "abc" */ ``` ### Managing Property Order The `propertyOrder` option provides control over the order of object fields in the output. This feature is particularly useful when the sequence of keys is important for the consuming processes or when maintaining the input order enhances readability and usability. By default, the `propertyOrder` option is set to `"none"`. This means that the internal system decides the order of keys to optimize parsing speed. The order of keys in this mode should not be considered stable, and it's recommended not to rely on key ordering as it may change in future updates. Setting `propertyOrder` to `"original"` ensures that the keys are ordered as they appear in the input during the decoding/encoding process. **Example** (Synchronous Decoding) ```ts twoslash import { Schema } from "effect" const schema = Schema.Struct({ a: Schema.Number, b: Schema.Literal("b"), c: Schema.Number }) // Default decoding, where property order is system-defined console.log(Schema.decodeUnknownSync(schema)({ b: "b", c: 2, a: 1 })) // Output may vary: { a: 1, b: 'b', c: 2 } // Decoding while preserving input order console.log( Schema.decodeUnknownSync(schema)( { b: "b", c: 2, a: 1 }, { propertyOrder: "original" } ) ) // Output preserves input order: { b: 'b', c: 2, a: 1 } ``` **Example** (Asynchronous Decoding) ```ts twoslash import type { Duration } from "effect" import { Effect, ParseResult, Schema } from "effect" // Helper function to simulate an async operation in schema const effectify = (duration: Duration.DurationInput) => Schema.Number.pipe( Schema.transformOrFail(Schema.Number, { strict: true, decode: (x) => Effect.sleep(duration).pipe( Effect.andThen(ParseResult.succeed(x)) ), encode: ParseResult.succeed }) ) // Define a structure with asynchronous behavior in each field const schema = Schema.Struct({ a: effectify("200 millis"), b: effectify("300 millis"), c: effectify("100 millis") }).annotations({ concurrency: 3 }) // Default decoding, where property order is system-defined Schema.decode(schema)({ a: 1, b: 2, c: 3 }) .pipe(Effect.runPromise) .then(console.log) // Output decided internally: { c: 3, a: 1, b: 2 } // Decoding while preserving input order Schema.decode(schema)({ a: 1, b: 2, c: 3 }, { propertyOrder: "original" }) .pipe(Effect.runPromise) .then(console.log) // Output preserving input order: { a: 1, b: 2, c: 3 } ``` ### Customizing Parsing Behavior at the Schema Level The `parseOptions` annotation allows you to customize parsing behavior at different schema levels, enabling you to apply unique parsing settings to nested schemas within a structure. Options defined within a schema override parent-level settings and apply to all nested schemas. **Example** (Using `parseOptions` to Customize Error Handling) ```ts twoslash import { Schema } from "effect" import { Either } from "effect" const schema = Schema.Struct({ a: Schema.Struct({ b: Schema.String, c: Schema.String }).annotations({ title: "first error only", // Limit errors to the first in this sub-schema parseOptions: { errors: "first" } }), d: Schema.String }).annotations({ title: "all errors", // Capture all errors for the main schema parseOptions: { errors: "all" } }) // Decode input with custom error-handling behavior const result = Schema.decodeUnknownEither(schema)( { a: {} }, { errors: "first" } ) if (Either.isLeft(result)) { console.log(result.left.message) } /* all errors ├─ ["a"] │ └─ first error only │ └─ ["b"] │ └─ is missing └─ ["d"] └─ is missing */ ``` **Detailed Output Explanation:** In this example: - The main schema is configured to display all errors. Hence, you will see errors related to both the `d` field (since it's missing) and any errors from the `a` subschema. - The subschema (`a`) is set to display only the first error. Although both `b` and `c` fields are missing, only the first missing field (`b`) is reported. ## Type Guards The `Schema.is` function provides a way to verify if a value conforms to a given schema. It acts as a [type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates), taking a value of type `unknown` and determining if it matches the structure and type constraints defined in the schema. Here's how the `Schema.is` function works: 1. **Schema Definition**: Define a schema to describe the structure and constraints of the data type you expect. For instance, `Schema`, where `Type` is the target type you want to validate against. 2. **Type Guard Creation**: Use the schema to create a user-defined type guard, `(u: unknown) => u is Type`. This function can be used at runtime to check if a value meets the requirements of the schema. **Example** (Creating and Using a Type Guard) ```ts twoslash import { Schema } from "effect" // Define a schema for a Person object const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) // Generate a type guard from the schema const isPerson = Schema.is(Person) // Test the type guard with various inputs console.log(isPerson({ name: "Alice", age: 30 })) // Output: true console.log(isPerson(null)) // Output: false console.log(isPerson({})) // Output: false ``` The generated `isPerson` function has the following signature: ```ts showLineNumbers=false const isPerson: ( u: unknown, overrideOptions?: number | ParseOptions ) => u is { readonly name: string readonly age: number } ``` ## Assertions While type guards verify whether a value conforms to a specific type, the `Schema.asserts` function goes further by asserting that an input matches the schema type `Type` (from `Schema`). If the input does not match the schema, it throws a detailed error, making it useful for runtime validation. **Example** (Creating and Using an Assertion) ```ts twoslash import { Schema } from "effect" // Define a schema for a Person object const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) // Generate an assertion function from the schema const assertsPerson: Schema.Schema.ToAsserts = Schema.asserts(Person) try { // Attempt to assert that the input matches the Person schema assertsPerson({ name: "Alice", age: "30" }) } catch (e) { console.error("The input does not match the schema:") console.error(e) } /* throws: The input does not match the schema: { _id: 'ParseError', message: '{ readonly name: string; readonly age: number }\n' + '└─ ["age"]\n' + ' └─ Expected number, actual "30"' } */ // This input matches the schema and will not throw an error assertsPerson({ name: "Alice", age: 30 }) ``` The `assertsPerson` function generated from the schema has the following signature: ```ts showLineNumbers=false const assertsPerson: ( input: unknown, overrideOptions?: number | ParseOptions ) => asserts input is { readonly name: string readonly age: number } ``` ## Managing Missing Properties When decoding, it's important to understand how missing properties are processed. By default, if a property is not present in the input, it is treated as if it were present with an `undefined` value. **Example** (Default Behavior of Missing Properties) ```ts twoslash import { Schema } from "effect" const schema = Schema.Struct({ a: Schema.Unknown }) const input = {} console.log(Schema.decodeUnknownSync(schema)(input)) // Output: { a: undefined } ``` In this example, although the key `"a"` is not present in the input, it is treated as `{ a: undefined }` by default. If you need your validation logic to differentiate between genuinely missing properties and those explicitly set to `undefined`, you can enable the `exact` option. **Example** (Setting `exact: true` to Distinguish Missing Properties) ```ts twoslash import { Schema } from "effect" const schema = Schema.Struct({ a: Schema.Unknown }) const input = {} console.log(Schema.decodeUnknownSync(schema)(input, { exact: true })) /* throws ParseError: { readonly a: unknown } └─ ["a"] └─ is missing */ ``` For the APIs `Schema.is` and `Schema.asserts`, however, the default behavior is to treat missing properties strictly, where the default for `exact` is `true`: **Example** (Strict Handling of Missing Properties with `Schema.is` and `Schema.asserts`) ```ts twoslash import type { SchemaAST } from "effect" import { Schema } from "effect" const schema = Schema.Struct({ a: Schema.Unknown }) const input = {} console.log(Schema.is(schema)(input)) // Output: false console.log(Schema.is(schema)(input, { exact: false })) // Output: true const asserts: ( u: unknown, overrideOptions?: SchemaAST.ParseOptions ) => asserts u is { readonly a: unknown } = Schema.asserts(schema) try { asserts(input) console.log("asserts passed") } catch (e: any) { console.error("asserts failed") console.error(e.message) } /* Output: asserts failed { readonly a: unknown } └─ ["a"] └─ is missing */ try { asserts(input, { exact: false }) console.log("asserts passed") } catch (e: any) { console.error("asserts failed") console.error(e.message) } // Output: asserts passed ``` ## Naming Conventions The naming conventions in `effect/Schema` are designed to be straightforward and logical, **focusing primarily on compatibility with JSON serialization**. This approach simplifies the understanding and use of schemas, especially for developers who are integrating web technologies where JSON is a standard data interchange format. ### Overview of Naming Strategies **JSON-Compatible Types** Schemas that naturally serialize to JSON-compatible formats are named directly after their data types. For instance: - `Schema.Date`: serializes JavaScript Date objects to ISO-formatted strings, a typical method for representing dates in JSON. - `Schema.Number`: used directly as it maps precisely to the JSON number type, requiring no special transformation to remain JSON-compatible. **Non-JSON-Compatible Types** When dealing with types that do not have a direct representation in JSON, the naming strategy incorporates additional details to indicate the necessary transformation. This helps in setting clear expectations about the schema's behavior: For instance: - `Schema.DateFromSelf`: indicates that the schema handles `Date` objects, which are not natively JSON-serializable. - `Schema.NumberFromString`: this naming suggests that the schema processes numbers that are initially represented as strings, emphasizing the transformation from string to number when decoding. The primary goal of these schemas is to ensure that domain objects can be easily serialized ("encoded") and deserialized ("decoded") for transmission over network connections, thus facilitating their transfer between different parts of an application or across different applications. ### Rationale While JSON's ubiquity justifies its primary consideration in naming, the conventions also accommodate serialization for other types of transport. For instance, converting a `Date` to a string is a universally useful method for various communication protocols, not just JSON. Thus, the selected naming conventions serve as sensible defaults that prioritize clarity and ease of use, facilitating the serialization and deserialization processes across diverse technological environments. # [Introduction to Effect Schema](https://effect.website/docs/schema/introduction/) ## Overview import { Aside } from "@astrojs/starlight/components" Welcome to the documentation for `effect/Schema`, a module for defining and using schemas to validate and transform data in TypeScript. The `effect/Schema` module allows you to define a `Schema` that provides a blueprint for describing the structure and data types of your data. Once defined, you can leverage this schema to perform a range of operations, including: | Operation | Description | | --------------- | ------------------------------------------------------------------------------------ | | Decoding | Transforming data from an input type `Encoded` to an output type `Type`. | | Encoding | Converting data from an output type `Type` back to an input type `Encoded`. | | Asserting | Verifying that a value adheres to the schema's output type `Type`. | | Standard Schema | Generate a [Standard Schema V1](https://standardschema.dev/). | | Arbitraries | Generate arbitraries for [fast-check](https://github.com/dubzzz/fast-check) testing. | | JSON Schemas | Create JSON Schemas based on defined schemas. | | Equivalence | Create [Equivalence](/docs/schema/equivalence/) based on defined schemas. | | Pretty printing | Support pretty printing for data structures. | ## Requirements - TypeScript 5.4 or newer. - The `strict` flag enabled in your `tsconfig.json` file. - (Optional) The `exactOptionalPropertyTypes` flag enabled in your `tsconfig.json` file. ```json showLineNumbers=false { "compilerOptions": { "strict": true, "exactOptionalPropertyTypes": true // optional } } ``` ### The exactOptionalPropertyTypes Option The `effect/Schema` module takes advantage of the `exactOptionalPropertyTypes` option of `tsconfig.json`. This option affects how optional properties are typed (to learn more about this option, you can refer to the official [TypeScript documentation](https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes)). **Example** (With `exactOptionalPropertyTypes` Enabled) ```ts twoslash import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.optionalWith(Schema.NonEmptyString, { exact: true }) }) type Type = Schema.Schema.Type /* type Type = { readonly name?: string; } */ // @ts-expect-error Schema.decodeSync(Person)({ name: undefined }) /* Argument of type '{ name: undefined; }' is not assignable to parameter of type '{ readonly name?: string; }' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. Types of property 'name' are incompatible. Type 'undefined' is not assignable to type 'string'.ts(2379) */ ``` Here, notice that the type of `name` is "exact" (`string`), which means the type checker will catch any attempt to assign an invalid value (like `undefined`). **Example** (With `exactOptionalPropertyTypes` Disabled) If, for some reason, you can't enable the `exactOptionalPropertyTypes` option (perhaps due to conflicts with other third-party libraries), you can still use `effect/Schema`. However, there will be a mismatch between the types and the runtime behavior: ```ts import { Schema } from "effect" const Person = Schema.Struct({ name: Schema.optionalWith(Schema.NonEmptyString, { exact: true }) }) type Type = Schema.Schema.Type /* type Type = { readonly name?: string | undefined; } */ // No type error, but a decoding failure occurs Schema.decodeSync(Person)({ name: undefined }) /* throws: ParseError: { readonly name?: NonEmptyString } └─ ["name"] └─ NonEmptyString └─ From side refinement failure └─ Expected string, actual undefined */ ``` In this case, the type of `name` is widened to `string | undefined`, which means the type checker won't catch the invalid value (`undefined`). However, during decoding, you'll encounter an error, indicating that `undefined` is not allowed. ## The Schema Type The `Schema` type represents an **immutable** value that describes the structure of your data. Here is the general form of a `Schema`: ```text showLineNumbers=false ┌─── Type of the decoded value │ ┌─── Encoded type (input/output) │ │ ┌─── Requirements (context) ▼ ▼ ▼ Schema ``` The `Schema` type has three type parameters with the following meanings: | Parameter | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Type** | Represents the type of value that a schema can succeed with during decoding. | | **Encoded** | Represents the type of value that a schema can succeed with during encoding. By default, it's equal to `Type` if not explicitly provided. | | **Requirements** | Similar to the [`Effect`](https://effect.website/docs/getting-started/the-effect-type) type, it represents the contextual data required by the schema to execute both decoding and encoding. If this type parameter is `never` (default if not explicitly provided), it means the schema has no requirements. | **Examples** - `Schema` (defaulted to `Schema`) represents a schema that decodes to `string`, encodes to `string`, and has no requirements. - `Schema` (defaulted to `Schema`) represents a schema that decodes to `number` from `string`, encodes a `number` to a `string`, and has no requirements. ## Understanding Schema Values **Immutability**. `Schema` values are immutable, and every function in the `effect/Schema` module produces a new `Schema` value. **Modeling Data Structure**. These values do not perform any actions themselves, they simply model or describe the structure of your data. **Interpretation by Compilers**. A `Schema` can be interpreted by various "compilers" into specific operations, depending on the compiler type (decoding, encoding, pretty printing, arbitraries, etc...). ## Understanding Decoding and Encoding When working with data in TypeScript, you often need to handle data coming from or being sent to external systems. This data may not always match the format or types you expect, especially when dealing with user input, data from APIs, or data stored in different formats. To handle these discrepancies, we use **decoding** and **encoding**. | Term | Description | | ------------ | ------------------------------------------------------------------------------------------------------------ | | **Decoding** | Used for parsing data from external sources where you have no control over the data format. | | **Encoding** | Used when sending data out to external sources, converting it to a format that is expected by those sources. | For instance, when working with forms in the frontend, you often receive untyped data in the form of strings. This data can be tampered with and does not natively support arrays or booleans. Decoding helps you validate and parse this data into more useful types like numbers, dates, and arrays. Encoding allows you to convert these types back into the string format expected by forms. Below is a diagram that shows the relationship between encoding and decoding using a `Schema`: ```text showLineNumbers=false ┌─────────┐ ┌───┐ ┌───┐ ┌─────────┐ | unknown | | A | | I | | unknown | └─────────┘ └───┘ └───┘ └─────────┘ | | | | | validate | | | |─────────────►│ | | | | | | | is | | | |─────────────►│ | | | | | | | asserts | | | |─────────────►│ | | | | | | | encodeUnknown| | | |─────────────────────────►| | | | | | encode | | |──────────►│ | | | | | decode | | | ◄─────────| | | | | | | decodeUnknown| | ◄────────────────────────| ``` We'll break down these concepts using an example with a `Schema`. This schema serves as a tool to transform a `string` into a `Date` and vice versa. ### Encoding When we talk about "encoding," we are referring to the process of changing a `Date` into a `string`. To put it simply, it's the act of converting data from one format to another. ### Decoding Conversely, "decoding" entails transforming a `string` back into a `Date`. It's essentially the reverse operation of encoding, where data is returned to its original form. ### Decoding From Unknown Decoding from `unknown` involves two key steps: 1. **Checking:** Initially, we verify that the input data (which is of the `unknown` type) matches the expected structure. In our specific case, this means ensuring that the input is indeed a `string`. 2. **Decoding:** Following the successful check, we proceed to convert the `string` into a `Date`. This process completes the decoding operation, where the data is both validated and transformed. ### Encoding From Unknown Encoding from `unknown` involves two key steps: 1. **Checking:** Initially, we verify that the input data (which is of the `unknown` type) matches the expected structure. In our specific case, this means ensuring that the input is indeed a `Date`. 2. **Encoding:** Following the successful check, we proceed to convert the `Date` into a `string`. This process completes the encoding operation, where the data is both validated and transformed. ## The Rule of Schemas When working with schemas, there's an important rule to keep in mind: your schemas should be crafted in a way that when you perform both encoding and decoding operations, you should end up with the original value. In simpler terms, if you encode a value and then immediately decode it, the result should match the original value you started with. This rule ensures that your data remains consistent and reliable throughout the encoding and decoding process. # [Schema to JSON Schema](https://effect.website/docs/schema/json-schema/) ## Overview The `JSONSchema.make` function allows you to generate a JSON Schema from a schema. **Example** (Creating a JSON Schema for a Struct) The following example defines a `Person` schema with properties for `name` (a string) and `age` (a number). It then generates the corresponding JSON Schema. ```ts twoslash import { JSONSchema, Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) const jsonSchema = JSONSchema.make(Person) console.log(JSON.stringify(jsonSchema, null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": [ "name", "age" ], "properties": { "name": { "type": "string" }, "age": { "type": "number" } }, "additionalProperties": false } */ ``` The `JSONSchema.make` function aims to produce an optimal JSON Schema representing the input part of the decoding phase. It does this by traversing the schema from the most nested component, incorporating each refinement, and **stops at the first transformation** encountered. **Example** (Excluding Transformations in JSON Schema) Consider modifying the `age` field to include both a refinement and a transformation. Only the refinement is reflected in the JSON Schema. ```ts twoslash import { JSONSchema, Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number.pipe( // Refinement included in the JSON Schema Schema.int(), // Transformation excluded from the JSON Schema Schema.clamp(1, 10) ) }) const jsonSchema = JSONSchema.make(Person) console.log(JSON.stringify(jsonSchema, null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": [ "name", "age" ], "properties": { "name": { "type": "string" }, "age": { "type": "integer", "description": "an integer", "title": "integer" } }, "additionalProperties": false } */ ``` In this case, the JSON Schema reflects the integer refinement but does not include the transformation that clamps the value. ## Specific Outputs for Schema Types ### Literals Literals are transformed into `enum` types within JSON Schema. **Example** (Single Literal) ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.Literal("a") console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "string", "enum": [ "a" ] } */ ``` **Example** (Union of literals) ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.Literal("a", "b") console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "string", "enum": [ "a", "b" ] } */ ``` ### Void ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.Void console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/schemas/void", "title": "void" } */ ``` ### Any ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.Any console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/schemas/any", "title": "any" } */ ``` ### Unknown ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.Unknown console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/schemas/unknown", "title": "unknown" } */ ``` ### Object ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.Object console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/schemas/object", "anyOf": [ { "type": "object" }, { "type": "array" } ], "description": "an object in the TypeScript meaning, i.e. the `object` type", "title": "object" } */ ``` ### String ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.String console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "string" } */ ``` ### Number ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.Number console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "number" } */ ``` ### Boolean ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.Boolean console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "boolean" } */ ``` ### Tuples ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.Tuple(Schema.String, Schema.Number) console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "array", "minItems": 2, "items": [ { "type": "string" }, { "type": "number" } ], "additionalItems": false } */ ``` ### Arrays ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.Array(Schema.String) console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "array", "items": { "type": "string" } } */ ``` ### Non Empty Arrays Represents an array with at least one element. **Example** ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.NonEmptyArray(Schema.String) console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "array", "minItems": 1, "items": { "type": "string" } } */ ``` ### Structs ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.Struct({ name: Schema.String, age: Schema.Number }) console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": [ "name", "age" ], "properties": { "name": { "type": "string" }, "age": { "type": "number" } }, "additionalProperties": false } */ ``` ### Records ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.Record({ key: Schema.String, value: Schema.Number }) console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": [], "properties": {}, "patternProperties": { "": { "type": "number" } } } */ ``` ### Mixed Structs with Records Combines fixed properties from a struct with dynamic properties from a record. **Example** ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.Struct( { name: Schema.String, age: Schema.Number }, Schema.Record({ key: Schema.String, value: Schema.Union(Schema.String, Schema.Number) }) ) console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": [ "name", "age" ], "properties": { "name": { "type": "string" }, "age": { "type": "number" } }, "patternProperties": { "": { "anyOf": [ { "type": "string" }, { "type": "number" } ] } } } */ ``` ### Enums ```ts twoslash import { JSONSchema, Schema } from "effect" enum Fruits { Apple, Banana } const schema = Schema.Enums(Fruits) console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "$comment": "/schemas/enums", "anyOf": [ { "type": "number", "title": "Apple", "enum": [ 0 ] }, { "type": "number", "title": "Banana", "enum": [ 1 ] } ] } */ ``` ### Template Literals ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.TemplateLiteral(Schema.Literal("a"), Schema.Number) console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "string", "title": "`a${number}`", "description": "a template literal", "pattern": "^a[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" } */ ``` ### Unions Unions are expressed using `anyOf` or `enum`, depending on the types involved: **Example** (Generic Union) ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.Union(Schema.String, Schema.Number) console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "anyOf": [ { "type": "string" }, { "type": "number" } ] } */ ``` **Example** (Union of literals) ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.Literal("a", "b") console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "string", "enum": [ "a", "b" ] } */ ``` ## Identifier Annotations You can add `identifier` annotations to schemas to improve structure and maintainability. Annotated schemas are included in a `$defs` object in the root of the JSON Schema and referenced from there. **Example** (Using Identifier Annotations) ```ts twoslash import { JSONSchema, Schema } from "effect" const Name = Schema.String.annotations({ identifier: "Name" }) const Age = Schema.Number.annotations({ identifier: "Age" }) const Person = Schema.Struct({ name: Name, age: Age }) const jsonSchema = JSONSchema.make(Person) console.log(JSON.stringify(jsonSchema, null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "$defs": { "Name": { "type": "string", "description": "a string", "title": "string" }, "Age": { "type": "number", "description": "a number", "title": "number" } }, "type": "object", "required": [ "name", "age" ], "properties": { "name": { "$ref": "#/$defs/Name" }, "age": { "$ref": "#/$defs/Age" } }, "additionalProperties": false } */ ``` By using identifier annotations, schemas can be reused and referenced more easily, especially in complex JSON Schemas. ## Standard JSON Schema Annotations Standard JSON Schema annotations such as `title`, `description`, `default`, and `examples` are supported. These annotations allow you to enrich your schemas with metadata that can enhance readability and provide additional information about the data structure. **Example** (Using Annotations for Metadata) ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.String.annotations({ description: "my custom description", title: "my custom title", default: "", examples: ["a", "b"] }) const jsonSchema = JSONSchema.make(schema) console.log(JSON.stringify(jsonSchema, null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "string", "description": "my custom description", "title": "my custom title", "examples": [ "a", "b" ], "default": "" } */ ``` ### Adding annotations to Struct properties To enhance the clarity of your JSON schemas, it's advisable to add annotations directly to the property signatures rather than to the type itself. This method is more semantically appropriate as it links descriptive titles and other metadata specifically to the properties they describe, rather than to the generic type. **Example** (Annotated Struct Properties) ```ts twoslash import { JSONSchema, Schema } from "effect" const Person = Schema.Struct({ firstName: Schema.propertySignature(Schema.String).annotations({ title: "First name" }), lastName: Schema.propertySignature(Schema.String).annotations({ title: "Last Name" }) }) const jsonSchema = JSONSchema.make(Person) console.log(JSON.stringify(jsonSchema, null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": [ "firstName", "lastName" ], "properties": { "firstName": { "type": "string", "title": "First name" }, "lastName": { "type": "string", "title": "Last Name" } }, "additionalProperties": false } */ ``` ## Recursive and Mutually Recursive Schemas Recursive and mutually recursive schemas are supported, however it's **mandatory** to use `identifier` annotations for these types of schemas to ensure correct references and definitions within the generated JSON Schema. **Example** (Recursive Schema with Identifier Annotations) In this example, the `Category` schema refers to itself, making it necessary to use an `identifier` annotation to facilitate the reference. ```ts twoslash import { JSONSchema, Schema } from "effect" // Define the interface representing a category structure interface Category { readonly name: string readonly categories: ReadonlyArray } // Define a recursive schema with a required identifier annotation const Category = Schema.Struct({ name: Schema.String, categories: Schema.Array( // Recursive reference to the Category schema Schema.suspend((): Schema.Schema => Category) ) }).annotations({ identifier: "Category" }) const jsonSchema = JSONSchema.make(Category) console.log(JSON.stringify(jsonSchema, null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "$defs": { "Category": { "type": "object", "required": [ "name", "categories" ], "properties": { "name": { "type": "string" }, "categories": { "type": "array", "items": { "$ref": "#/$defs/Category" } } }, "additionalProperties": false } }, "$ref": "#/$defs/Category" } */ ``` ## Customizing JSON Schema Generation When working with JSON Schema certain data types, such as `bigint`, lack a direct representation because JSON Schema does not natively support them. This absence typically leads to an error when the schema is generated. **Example** (Error Due to Missing Annotation) Attempting to generate a JSON Schema for unsupported types like `bigint` will lead to a missing annotation error: ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.Struct({ a_bigint_field: Schema.BigIntFromSelf }) const jsonSchema = JSONSchema.make(schema) console.log(JSON.stringify(jsonSchema, null, 2)) /* throws: Error: Missing annotation at path: ["a_bigint_field"] details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation schema (BigIntKeyword): bigint */ ``` To address this, you can enhance the schema with a custom `jsonSchema` annotation, defining how you intend to represent such types in JSON Schema: **Example** (Using Custom Annotation for Unsupported Type) ```ts twoslash import { JSONSchema, Schema } from "effect" const schema = Schema.Struct({ // Adding a custom JSON Schema annotation for the `bigint` type a_bigint_field: Schema.BigIntFromSelf.annotations({ jsonSchema: { type: "some custom way to represent a bigint in JSON Schema" } }) }) const jsonSchema = JSONSchema.make(schema) console.log(JSON.stringify(jsonSchema, null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": [ "a_bigint_field" ], "properties": { "a_bigint_field": { "type": "some custom way to represent a bigint in JSON Schema" } }, "additionalProperties": false } */ ``` ### Refinements When defining a refinement (e.g., through the `Schema.filter` function), you can include a JSON Schema annotation to describe the refinement. This annotation is added as a "fragment" that becomes part of the generated JSON Schema. If a schema contains multiple refinements, their respective annotations are merged into the output. **Example** (Using Refinements with Merged Annotations) ```ts twoslash import { JSONSchema, Schema } from "effect" // Define a schema with a refinement for positive numbers const Positive = Schema.Number.pipe( Schema.filter((n) => n > 0, { jsonSchema: { minimum: 0 } }) ) // Add an upper bound refinement to the schema const schema = Positive.pipe( Schema.filter((n) => n <= 10, { jsonSchema: { maximum: 10 } }) ) const jsonSchema = JSONSchema.make(schema) console.log(JSON.stringify(jsonSchema, null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "number", "minimum": 0, "maximum": 10 } */ ``` The `jsonSchema` annotation is defined as a generic object, allowing it to represent non-standard extensions. This flexibility leaves the responsibility of enforcing type constraints to the user. If you prefer stricter type enforcement or need to support non-standard extensions, you can introduce a `satisfies` constraint on the object literal. This constraint should be used in conjunction with the typing library of your choice. **Example** (Ensuring Type Correctness) In the following example, we've used the `@types/json-schema` package to provide TypeScript definitions for JSON Schema. This approach not only ensures type correctness but also enables autocomplete suggestions in your IDE. ```ts twoslash import { JSONSchema, Schema } from "effect" import type { JSONSchema7 } from "json-schema" const Positive = Schema.Number.pipe( Schema.filter((n) => n > 0, { jsonSchema: { minimum: 0 } // Generic object, no type enforcement }) ) const schema = Positive.pipe( Schema.filter((n) => n <= 10, { jsonSchema: { maximum: 10 } satisfies JSONSchema7 // Enforces type constraints }) ) const jsonSchema = JSONSchema.make(schema) console.log(JSON.stringify(jsonSchema, null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "number", "minimum": 0, "maximum": 10 } */ ``` For schema types other than refinements, you can override the default generated JSON Schema by providing a custom `jsonSchema` annotation. The content of this annotation will replace the system-generated schema. **Example** (Custom Annotation for a Struct) ```ts twoslash import { JSONSchema, Schema } from "effect" // Define a struct with a custom JSON Schema annotation const schema = Schema.Struct({ foo: Schema.String }).annotations({ jsonSchema: { type: "object" } }) const jsonSchema = JSONSchema.make(schema) console.log(JSON.stringify(jsonSchema, null, 2)) /* Output { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" } the default would be: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": [ "foo" ], "properties": { "foo": { "type": "string" } }, "additionalProperties": false } */ ``` ## Specialized JSON Schema Generation with Schema.parseJson The `Schema.parseJson` function provides a unique approach to JSON Schema generation. Instead of defaulting to a schema for a plain string, which represents the "from" side of the transformation, it generates a schema based on the structure provided within the argument. This behavior ensures that the generated JSON Schema reflects the intended structure of the parsed data, rather than the raw JSON input. **Example** (Generating JSON Schema for a Parsed Object) ```ts twoslash import { JSONSchema, Schema } from "effect" // Define a schema that parses a JSON string into a structured object const schema = Schema.parseJson( Schema.Struct({ // Nested parsing: JSON string to a number a: Schema.parseJson(Schema.NumberFromString) }) ) const jsonSchema = JSONSchema.make(schema) console.log(JSON.stringify(jsonSchema, null, 2)) /* Output: { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": [ "a" ], "properties": { "a": { "type": "string", "contentMediaType": "application/json" } }, "additionalProperties": false } */ ``` # [Schema to Pretty Printer](https://effect.website/docs/schema/pretty/) ## Overview The `Pretty.make` function is used to create pretty printers that generate a formatted string representation of values based on a schema. **Example** (Pretty Printer for a Struct Schema) ```ts twoslash import { Pretty, Schema } from "effect" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) // Create a pretty printer for the schema const PersonPretty = Pretty.make(Person) // Format and print a Person object console.log(PersonPretty({ name: "Alice", age: 30 })) /* Output: '{ "name": "Alice", "age": 30 }' */ ``` ## Customizing Pretty Printer Generation You can customize how the pretty printer formats output by using the `pretty` annotation within your schema definition. The `pretty` annotation takes any type parameters provided (`typeParameters`) and formats the value into a string. **Example** (Custom Pretty Printer for Numbers) ```ts twoslash import { Pretty, Schema } from "effect" // Define a schema with a custom pretty annotation const schema = Schema.Number.annotations({ pretty: (/**typeParameters**/) => (value) => `my format: ${value}` }) // Create the pretty printer const customPrettyPrinter = Pretty.make(schema) // Format and print a value console.log(customPrettyPrinter(1)) // Output: "my format: 1" ``` # [Schema Projections](https://effect.website/docs/schema/projections/) ## Overview Sometimes, you may want to create a new schema based on an existing one, focusing specifically on either its `Type` or `Encoded` aspect. The Schema module provides several functions to make this possible. ## typeSchema The `Schema.typeSchema` function is used to extract the `Type` portion of a schema, resulting in a new schema that retains only the type-specific properties from the original schema. This excludes any initial encoding or transformation logic applied to the original schema. **Function Signature** ```ts showLineNumbers=false declare const typeSchema: (schema: Schema) => Schema ``` **Example** (Extracting Only Type-Specific Properties) ```ts twoslash import { Schema } from "effect" const Original = Schema.Struct({ quantity: Schema.NumberFromString.pipe(Schema.greaterThanOrEqualTo(2)) }) // This creates a schema where 'quantity' is defined as a number // that must be greater than or equal to 2. const TypeSchema = Schema.typeSchema(Original) // TypeSchema is equivalent to: const TypeSchema2 = Schema.Struct({ quantity: Schema.Number.pipe(Schema.greaterThanOrEqualTo(2)) }) ``` ## encodedSchema The `Schema.encodedSchema` function enables you to extract the `Encoded` portion of a schema, creating a new schema that matches the original properties but **omits any refinements or transformations** applied to the schema. **Function Signature** ```ts showLineNumbers=false declare const encodedSchema: ( schema: Schema ) => Schema ``` **Example** (Extracting Encoded Properties Only) ```ts twoslash import { Schema } from "effect" const Original = Schema.Struct({ quantity: Schema.String.pipe(Schema.minLength(3)) }) // This creates a schema where 'quantity' is just a string, // disregarding the minLength refinement. const Encoded = Schema.encodedSchema(Original) // Encoded is equivalent to: const Encoded2 = Schema.Struct({ quantity: Schema.String }) ``` ## encodedBoundSchema The `Schema.encodedBoundSchema` function is similar to `Schema.encodedSchema` but preserves the refinements up to the first transformation point in the original schema. **Function Signature** ```ts showLineNumbers=false declare const encodedBoundSchema: ( schema: Schema ) => Schema ``` The term "bound" in this context refers to the boundary up to which refinements are preserved when extracting the encoded form of a schema. It essentially marks the limit to which initial validations and structure are maintained before any transformations are applied. **Example** (Retaining Initial Refinements Only) ```ts twoslash import { Schema } from "effect" const Original = Schema.Struct({ foo: Schema.String.pipe( Schema.minLength(3), Schema.compose(Schema.Trim) ) }) // The EncodedBoundSchema schema preserves the minLength(3) refinement, // ensuring the string length condition is enforced // but omits the Schema.Trim transformation. const EncodedBoundSchema = Schema.encodedBoundSchema(Original) // EncodedBoundSchema is equivalent to: const EncodedBoundSchema2 = Schema.Struct({ foo: Schema.String.pipe(Schema.minLength(3)) }) ``` # [Schema to Standard Schema](https://effect.website/docs/schema/standard-schema/) ## Overview import { Aside } from "@astrojs/starlight/components" The `Schema.standardSchemaV1` API allows you to generate a [Standard Schema v1](https://standardschema.dev/) object from an Effect `Schema`. **Example** (Generating a Standard Schema V1) ```ts twoslash import { Schema } from "effect" const schema = Schema.Struct({ name: Schema.String }) // Convert an Effect schema into a Standard Schema V1 object // // ┌─── StandardSchemaV1<{ readonly name: string; }> // ▼ const standardSchema = Schema.standardSchemaV1(schema) ``` ## Sync vs Async Validation The `Schema.standardSchemaV1` API creates a schema whose `validate` method attempts to decode and validate the provided input synchronously. If the underlying `Schema` includes any asynchronous components (e.g., asynchronous message resolutions or checks), then validation will necessarily return a `Promise` instead. **Example** (Handling Synchronous and Asynchronous Validation) ```ts twoslash import { Effect, Schema } from "effect" // Utility function to display sync and async results const print = (t: T) => t instanceof Promise ? t.then((x) => console.log("Promise", JSON.stringify(x, null, 2))) : console.log("Value", JSON.stringify(t, null, 2)) // Define a synchronous schema const sync = Schema.Struct({ name: Schema.String }) // Generate a Standard Schema V1 object const syncStandardSchema = Schema.standardSchemaV1(sync) // Validate synchronously print(syncStandardSchema["~standard"].validate({ name: null })) /* Output: { "issues": [ { "path": [ "name" ], "message": "Expected string, actual null" } ] } */ // Define an asynchronous schema with a transformation const async = Schema.transformOrFail( sync, Schema.Struct({ name: Schema.NonEmptyString }), { // Simulate an asynchronous validation delay decode: (x) => Effect.sleep("100 millis").pipe(Effect.as(x)), encode: Effect.succeed } ) // Generate a Standard Schema V1 object const asyncStandardSchema = Schema.standardSchemaV1(async) // Validate asynchronously print(asyncStandardSchema["~standard"].validate({ name: "" })) /* Output: Promise { "issues": [ { "path": [ "name" ], "message": "Expected a non empty string, actual \"\"" } ] } */ ``` ## Defects If an unexpected defect occurs during validation, it is reported as a single issue without a `path`. This ensures that unexpected errors do not disrupt schema validation but are still captured and reported. **Example** (Handling Defects) ```ts twoslash import { Effect, Schema } from "effect" // Define a schema with a defect in the decode function const defect = Schema.transformOrFail(Schema.String, Schema.String, { // Simulate an internal failure decode: () => Effect.die("Boom!"), encode: Effect.succeed }) // Generate a Standard Schema V1 object const defectStandardSchema = Schema.standardSchemaV1(defect) // Validate input, triggering a defect console.log(defectStandardSchema["~standard"].validate("a")) /* Output: { issues: [ { message: 'Error: Boom!' } ] } */ ``` # [Sink Concurrency](https://effect.website/docs/sink/concurrency/) ## Overview This section covers concurrent operations that allow multiple sinks to run simultaneously. These can be valuable for enhancing task performance when concurrent execution is desired. ## Combining Results with Concurrent Zipping To run two sinks concurrently and combine their results, use `Sink.zip`. This operation executes both sinks concurrently and combines their outcomes into a tuple. **Example** (Running Two Sinks Concurrently and Combining Results) ```ts twoslash import { Sink, Console, Stream, Schedule, Effect } from "effect" const stream = Stream.make("1", "2", "3", "4", "5").pipe( Stream.schedule(Schedule.spaced("10 millis")) ) const sink1 = Sink.forEach((s: string) => Console.log(`sink 1: ${s}`) ).pipe(Sink.as(1)) const sink2 = Sink.forEach((s: string) => Console.log(`sink 2: ${s}`) ).pipe(Sink.as(2)) // Combine the two sinks to run concurrently and collect results in a tuple const sink = Sink.zip(sink1, sink2, { concurrent: true }) Effect.runPromise(Stream.run(stream, sink)).then(console.log) /* Output: sink 1: 1 sink 2: 1 sink 1: 2 sink 2: 2 sink 1: 3 sink 2: 3 sink 1: 4 sink 2: 4 sink 1: 5 sink 2: 5 [ 1, 2 ] */ ``` ## Racing Sinks: First Completion Wins The `Sink.race` operation allows multiple sinks to compete for completion. The first sink to finish provides the result. **Example** (Racing Two Sinks to Capture the First Result) ```ts twoslash import { Sink, Console, Stream, Schedule, Effect } from "effect" const stream = Stream.make("1", "2", "3", "4", "5").pipe( Stream.schedule(Schedule.spaced("10 millis")) ) const sink1 = Sink.forEach((s: string) => Console.log(`sink 1: ${s}`) ).pipe(Sink.as(1)) const sink2 = Sink.forEach((s: string) => Console.log(`sink 2: ${s}`) ).pipe(Sink.as(2)) // Race the two sinks, the result will be from the first to complete const sink = Sink.race(sink1, sink2) Effect.runPromise(Stream.run(stream, sink)).then(console.log) /* Output: sink 1: 1 sink 2: 1 sink 1: 2 sink 2: 2 sink 1: 3 sink 2: 3 sink 1: 4 sink 2: 4 sink 1: 5 sink 2: 5 1 */ ``` # [Schema Transformations](https://effect.website/docs/schema/transformations/) ## Overview import { Aside } from "@astrojs/starlight/components" Transformations are important when working with schemas. They allow you to change data from one type to another. For example, you might parse a string into a number or convert a date string into a `Date` object. The [Schema.transform](#transform) and [Schema.transformOrFail](#transformorfail) functions help you connect two schemas so you can convert data between them. ## transform `Schema.transform` creates a new schema by taking the output of one schema (the "source") and making it the input of another schema (the "target"). Use this when you know the transformation will always succeed. If it might fail, use [Schema.transformOrFail](#transformorfail) instead. ### Understanding Input and Output "Output" and "input" depend on what you are doing (decoding or encoding): **When decoding:** - The source schema `Schema` produces a `SourceType`. - The target schema `Schema` expects a `TargetEncoded`. - The decoding path looks like this: `SourceEncoded` → `TargetType`. If `SourceType` and `TargetEncoded` differ, you can provide a `decode` function to convert the source schema's output into the target schema's input. **When encoding:** - The target schema `Schema` produces a `TargetEncoded`. - The source schema `Schema` expects a `SourceType`. - The encoding path looks like this: `TargetType` → `SourceEncoded`. If `TargetEncoded` and `SourceType` differ, you can provide an `encode` function to convert the target schema's output into the source schema's input. ### Combining Two Primitive Schemas In this example, we start with a schema that accepts `"on"` or `"off"` and transform it into a boolean schema. The `decode` function turns `"on"` into `true` and `"off"` into `false`. The `encode` function does the reverse. This gives us a `Schema`. **Example** (Converting a String to a Boolean) ```ts twoslash import { Schema } from "effect" // Convert "on"/"off" to boolean and back const BooleanFromString = Schema.transform( // Source schema: "on" or "off" Schema.Literal("on", "off"), // Target schema: boolean Schema.Boolean, { // optional but you get better error messages from TypeScript strict: true, // Transformation to convert the output of the // source schema ("on" | "off") into the input of the // target schema (boolean) decode: (literal) => literal === "on", // Always succeeds here // Reverse transformation encode: (bool) => (bool ? "on" : "off") } ) // ┌─── "on" | "off" // ▼ type Encoded = typeof BooleanFromString.Encoded // ┌─── boolean // ▼ type Type = typeof BooleanFromString.Type console.log(Schema.decodeUnknownSync(BooleanFromString)("on")) // Output: true ``` The `decode` function above never fails by itself. However, the full decoding process can still fail if the input does not fit the source schema. For example, if you provide `"wrong"` instead of `"on"` or `"off"`, the source schema will fail before calling `decode`. **Example** (Handling Invalid Input) ```ts twoslash collapse={4-12} import { Schema } from "effect" // Convert "on"/"off" to boolean and back const BooleanFromString = Schema.transform( Schema.Literal("on", "off"), Schema.Boolean, { strict: true, decode: (s) => s === "on", encode: (bool) => (bool ? "on" : "off") } ) // Providing input not allowed by the source schema Schema.decodeUnknownSync(BooleanFromString)("wrong") /* throws: ParseError: ("on" | "off" <-> boolean) └─ Encoded side transformation failure └─ "on" | "off" ├─ Expected "on", actual "wrong" └─ Expected "off", actual "wrong" */ ``` ### Combining Two Transformation Schemas Below is an example where both the source and target schemas transform their data: - The source schema is `Schema.NumberFromString`, which is `Schema`. - The target schema is `BooleanFromString` (defined above), which is `Schema`. This example involves four types and requires two conversions: - When decoding, convert a `number` into `"on" | "off"`. For example, treat any positive number as `"on"`. - When encoding, convert `"on" | "off"` back into a `number`. For example, treat `"on"` as `1` and `"off"` as `-1`. By composing these transformations, we get a schema that decodes a string into a boolean and encodes a boolean back into a string. The resulting schema is `Schema`. **Example** (Combining Two Transformation Schemas) ```ts twoslash collapse={4-12} import { Schema } from "effect" // Convert "on"/"off" to boolean and back const BooleanFromString = Schema.transform( Schema.Literal("on", "off"), Schema.Boolean, { strict: true, decode: (s) => s === "on", encode: (bool) => (bool ? "on" : "off") } ) const BooleanFromNumericString = Schema.transform( // Source schema: Convert string -> number Schema.NumberFromString, // Target schema: Convert "on"/"off" -> boolean BooleanFromString, { strict: true, // If number is positive, use "on", otherwise "off" decode: (n) => (n > 0 ? "on" : "off"), // If boolean is true, use 1, otherwise -1 encode: (bool) => (bool ? 1 : -1) } ) // ┌─── string // ▼ type Encoded = typeof BooleanFromNumericString.Encoded // ┌─── boolean // ▼ type Type = typeof BooleanFromNumericString.Type console.log(Schema.decodeUnknownSync(BooleanFromNumericString)("100")) // Output: true ``` **Example** (Converting an array to a ReadonlySet) In this example, we convert an array into a `ReadonlySet`. The `decode` function takes an array and creates a new `ReadonlySet`. The `encode` function converts the set back into an array. We also provide the schema of the array items so they are properly validated. ```ts twoslash import { Schema } from "effect" // This function builds a schema that converts between a readonly array // and a readonly set of items const ReadonlySetFromArray = ( itemSchema: Schema.Schema ): Schema.Schema, ReadonlyArray, R> => Schema.transform( // Source schema: array of items Schema.Array(itemSchema), // Target schema: readonly set of items // **IMPORTANT** We use `Schema.typeSchema` here to obtain the schema // of the items to avoid decoding the elements twice Schema.ReadonlySetFromSelf(Schema.typeSchema(itemSchema)), { strict: true, decode: (items) => new Set(items), encode: (set) => Array.from(set.values()) } ) const schema = ReadonlySetFromArray(Schema.String) // ┌─── readonly string[] // ▼ type Encoded = typeof schema.Encoded // ┌─── ReadonlySet // ▼ type Type = typeof schema.Type console.log(Schema.decodeUnknownSync(schema)(["a", "b", "c"])) // Output: Set(3) { 'a', 'b', 'c' } console.log(Schema.encodeSync(schema)(new Set(["a", "b", "c"]))) // Output: [ 'a', 'b', 'c' ] ``` ### Non-strict option In some cases, strict type checking can create issues during data transformations, especially when the types might slightly differ in specific transformations. To address these scenarios, `Schema.transform` offers the option `strict: false`, which relaxes type constraints and allows more flexible transformations. **Example** (Creating a Clamping Constructor) Let's consider the scenario where you need to define a constructor `clamp` that ensures a number falls within a specific range. This function returns a schema that "clamps" a number to a specified minimum and maximum range: ```ts twoslash import { Schema, Number } from "effect" const clamp = (minimum: number, maximum: number) => (self: Schema.Schema) => Schema.transform( // Source schema self, // Target schema: filter based on min/max range self.pipe( Schema.typeSchema, Schema.filter((a) => a <= minimum || a >= maximum) ), // @ts-expect-error { strict: true, // Clamp the number within the specified range decode: (a) => Number.clamp(a, { minimum, maximum }), encode: (a) => a } ) ``` In this example, `Number.clamp` returns a `number` that might not be recognized as the specific `A` type. This leads to a type mismatch under strict checking: ```text showLineNumbers=false Argument of type '{ strict: true; decode: (a: A) => number; encode: (a: A) => A; }' is not assignable to parameter of type '{ readonly decode: (fromA: A, fromI: I) => A; readonly encode: (toI: A, toA: A) => A; readonly strict?: true; } | { readonly decode: (fromA: A, fromI: I) => unknown; readonly encode: (toI: A, toA: A) => unknown; readonly strict: false; }'. The types returned by 'decode(...)' are incompatible between these types. Type 'number' is not assignable to type 'A'. 'number' is assignable to the constraint of type 'A', but 'A' could be instantiated with a different subtype of constraint 'number'.ts(2345) ``` There are two ways to resolve this issue: 1. **Using Type Assertion**: Adding a type cast can enforce the return type to be treated as type `A`: ```ts showLineNumbers=false decode: (a) => Number.clamp(a, { minimum, maximum }) as A ``` 2. **Using the Non-Strict Option**: Setting `strict: false` in the transformation options allows the schema to bypass some of TypeScript's type-checking rules, accommodating the type discrepancy: ```ts twoslash import { Schema, Number } from "effect" const clamp = (minimum: number, maximum: number) => (self: Schema.Schema) => Schema.transform( self, self.pipe( Schema.typeSchema, Schema.filter((a) => a >= minimum && a <= maximum) ), { strict: false, decode: (a) => Number.clamp(a, { minimum, maximum }), encode: (a) => a } ) ``` ## transformOrFail While the [Schema.transform](#transform) function is suitable for error-free transformations, the `Schema.transformOrFail` function is designed for more complex scenarios where **transformations can fail** during the decoding or encoding stages. This function enables decoding/encoding functions to return either a successful result or an error, making it particularly useful for validating and processing data that might not always conform to expected formats. ### Error Handling The `Schema.transformOrFail` function utilizes the ParseResult module to manage potential errors: | Constructor | Description | | --------------------- | ------------------------------------------------------------------------------------------------ | | `ParseResult.succeed` | Indicates a successful transformation, where no errors occurred. | | `ParseResult.fail` | Signals a failed transformation, creating a new `ParseError` based on the provided `ParseIssue`. | Additionally, the ParseResult module provides constructors for dealing with various types of parse issues, such as: | Parse Issue Type | Description | | ---------------- | --------------------------------------------------------------------------------------------- | | `Type` | Indicates a type mismatch error. | | `Missing` | Used when a required field is missing. | | `Unexpected` | Used for unexpected fields that are not allowed in the schema. | | `Forbidden` | Flags the decoding or encoding operation being forbidden by the schema. | | `Pointer` | Points to a specific location in the data where an issue occurred. | | `Refinement` | Used when a value does not meet a specific refinement or constraint. | | `Transformation` | Flags issues that occur during transformation from one type to another. | | `Composite` | Represents a composite error, combining multiple issues into one, helpful for grouped errors. | These tools allow for detailed and specific error handling, enhancing the reliability of data processing operations. **Example** (Converting a String to a Number) A common use case for `Schema.transformOrFail` is converting string representations of numbers into actual numeric types. This scenario is typical when dealing with user inputs or data from external sources. ```ts twoslash import { ParseResult, Schema } from "effect" export const NumberFromString = Schema.transformOrFail( // Source schema: accepts any string Schema.String, // Target schema: expects a number Schema.Number, { // optional but you get better error messages from TypeScript strict: true, decode: (input, options, ast) => { const parsed = parseFloat(input) // If parsing fails (NaN), return a ParseError with a custom error if (isNaN(parsed)) { return ParseResult.fail( // Create a Type Mismatch error new ParseResult.Type( // Provide the schema's abstract syntax tree for context ast, // Include the problematic input input, // Optional custom error message "Failed to convert string to number" ) ) } return ParseResult.succeed(parsed) }, encode: (input, options, ast) => ParseResult.succeed(input.toString()) } ) // ┌─── string // ▼ type Encoded = typeof NumberFromString.Encoded // ┌─── number // ▼ type Type = typeof NumberFromString.Type console.log(Schema.decodeUnknownSync(NumberFromString)("123")) // Output: 123 console.log(Schema.decodeUnknownSync(NumberFromString)("-")) /* throws: ParseError: (string <-> number) └─ Transformation process failure └─ Failed to convert string to number */ ``` Both `decode` and `encode` functions not only receive the value to transform (`input`), but also the [parse options](/docs/schema/getting-started/#parse-options) that the user sets when using the resulting schema, and the `ast`, which represents the low level definition of the schema you're transforming. ### Async Transformations In modern applications, especially those interacting with external APIs, you might need to transform data asynchronously. `Schema.transformOrFail` supports asynchronous transformations by allowing you to return an `Effect`. **Example** (Validating Data with an API Call) Consider a scenario where you need to validate a person's ID by making an API call. Here's how you can implement it: ```ts twoslash import { Effect, Schema, ParseResult } from "effect" // Define a function to make API requests const get = (url: string): Effect.Effect => Effect.tryPromise({ try: () => fetch(url).then((res) => { if (res.ok) { return res.json() as Promise } throw new Error(String(res.status)) }), catch: (e) => new Error(String(e)) }) // Create a branded schema for a person's ID const PeopleId = Schema.String.pipe(Schema.brand("PeopleId")) // Define a schema with async transformation const PeopleIdFromString = Schema.transformOrFail( Schema.String, PeopleId, { strict: true, decode: (s, _, ast) => // Make an API call to validate the ID Effect.mapBoth(get(`https://swapi.dev/api/people/${s}`), { // Error handling for failed API call onFailure: (e) => new ParseResult.Type(ast, s, e.message), // Return the ID if the API call succeeds onSuccess: () => s }), encode: ParseResult.succeed } ) // ┌─── string // ▼ type Encoded = typeof PeopleIdFromString.Encoded // ┌─── string & Brand<"PeopleId"> // ▼ type Type = typeof PeopleIdFromString.Type // ┌─── never // ▼ type Context = typeof PeopleIdFromString.Context // Run a successful decode operation Effect.runPromiseExit(Schema.decodeUnknown(PeopleIdFromString)("1")).then( console.log ) /* Output: { _id: 'Exit', _tag: 'Success', value: '1' } */ // Run a decode operation that will fail Effect.runPromiseExit( Schema.decodeUnknown(PeopleIdFromString)("fail") ).then(console.log) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: { _id: 'ParseError', message: '(string <-> string & Brand<"PeopleId">)\n' + '└─ Transformation process failure\n' + ' └─ Error: 404' } } } */ ``` ### Declaring Dependencies In cases where your transformation depends on external services, you can inject these services in the `decode` or `encode` functions. These dependencies are then tracked in the `Requirements` channel of the schema: ```text showLineNumbers=false "Requirements" Schema ``` **Example** (Validating Data with a Service) ```ts twoslash {46} import { Context, Effect, Schema, ParseResult, Layer } from "effect" // Define a Validation service for dependency injection class Validation extends Context.Tag("Validation")< Validation, { readonly validatePeopleid: (s: string) => Effect.Effect } >() {} // Create a branded schema for a person's ID const PeopleId = Schema.String.pipe(Schema.brand("PeopleId")) // Transform a string into a validated PeopleId, // using an external validation service const PeopleIdFromString = Schema.transformOrFail( Schema.String, PeopleId, { strict: true, decode: (s, _, ast) => // Asynchronously validate the ID using the injected service Effect.gen(function* () { // Access the validation service const validator = yield* Validation // Use service to validate ID yield* validator.validatePeopleid(s) return s }).pipe( Effect.mapError((e) => new ParseResult.Type(ast, s, e.message)) ), encode: ParseResult.succeed // Encode by simply returning the string } ) // ┌─── string // ▼ type Encoded = typeof PeopleIdFromString.Encoded // ┌─── string & Brand<"PeopleId"> // ▼ type Type = typeof PeopleIdFromString.Type // ┌─── Validation // ▼ type Context = typeof PeopleIdFromString.Context // Layer to provide a successful validation service const SuccessTest = Layer.succeed(Validation, { validatePeopleid: (_) => Effect.void }) // Run a successful decode operation Effect.runPromiseExit( Schema.decodeUnknown(PeopleIdFromString)("1").pipe( Effect.provide(SuccessTest) ) ).then(console.log) /* Output: { _id: 'Exit', _tag: 'Success', value: '1' } */ // Layer to provide a failing validation service const FailureTest = Layer.succeed(Validation, { validatePeopleid: (_) => Effect.fail(new Error("404")) }) // Run a decode operation that will fail Effect.runPromiseExit( Schema.decodeUnknown(PeopleIdFromString)("fail").pipe( Effect.provide(FailureTest) ) ).then(console.log) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: { _id: 'ParseError', message: '(string <-> string & Brand<"PeopleId">)\n' + '└─ Transformation process failure\n' + ' └─ Error: 404' } } } */ ``` ## One-Way Transformations with Forbidden Encoding In some cases, encoding a value back to its original form may not make sense or may be undesirable. You can use `Schema.transformOrFail` to define a one-way transformation and explicitly return a `Forbidden` parse error during the encoding process. This ensures that once a value is transformed, it cannot be reverted to its original form. **Example** (Password Hashing with Forbidden Encoding) Consider a scenario where you need to hash a user's plain text password for secure storage. It is important that the hashed password cannot be reversed back to plain text. By using `Schema.transformOrFail`, you can enforce this restriction, ensuring a one-way transformation from plain text to a hashed password. ```ts twoslash import { Schema, ParseResult, Redacted } from "effect" import { createHash } from "node:crypto" // Define a schema for plain text passwords // with a minimum length requirement const PlainPassword = Schema.String.pipe( Schema.minLength(6), Schema.brand("PlainPassword", { identifier: "PlainPassword" }) ) // Define a schema for hashed passwords as a separate branded type const HashedPassword = Schema.String.pipe( Schema.brand("HashedPassword", { identifier: "HashedPassword" }) ) // Define a one-way transformation from plain passwords to hashed passwords export const PasswordHashing = Schema.transformOrFail( PlainPassword, // Wrap the output in Redacted for added safety Schema.RedactedFromSelf(HashedPassword), { strict: true, // Decode: Transform a plain password into a hashed password decode: (plainPassword) => { const hash = createHash("sha256") .update(plainPassword) .digest("hex") // Wrap the hash in Redacted return ParseResult.succeed(Redacted.make(hash)) }, // Encode: Forbid reversing the hashed password back to plain text encode: (hashedPassword, _, ast) => ParseResult.fail( new ParseResult.Forbidden( ast, hashedPassword, "Encoding hashed passwords back to plain text is forbidden." ) ) } ) // ┌─── string // ▼ type Encoded = typeof PasswordHashing.Encoded // ┌─── Redacted> // ▼ type Type = typeof PasswordHashing.Type // Example: Decoding a plain password into a hashed password console.log( Schema.decodeUnknownSync(PasswordHashing)("myPlainPassword123") ) // Output: // Example: Attempting to encode a hashed password back to plain text console.log( Schema.encodeUnknownSync(PasswordHashing)(Redacted.make("2ef2b7...")) ) /* throws: ParseError: (PlainPassword <-> Redacted()) └─ Transformation process failure └─ (PlainPassword <-> Redacted()) └─ Encoding hashed passwords back to plain text is forbidden. */ ``` ## Composition Combining and reusing schemas is often needed in complex applications, and the `Schema.compose` combinator provides an efficient way to do this. With `Schema.compose`, you can chain two schemas, `Schema` and `Schema`, into a single schema `Schema`: **Example** (Composing Schemas to Parse a Delimited String into Numbers) ```ts twoslash import { Schema } from "effect" // Schema to split a string by commas into an array of strings // // ┌─── Schema // ▼ const schema1 = Schema.asSchema(Schema.split(",")) // Schema to convert an array of strings to an array of numbers // // ┌─── Schema // ▼ const schema2 = Schema.asSchema(Schema.Array(Schema.NumberFromString)) // Composed schema that takes a string, splits it by commas, // and converts the result into an array of numbers // // ┌─── Schema // ▼ const ComposedSchema = Schema.asSchema(Schema.compose(schema1, schema2)) ``` ### Non-strict Option When composing schemas, you may encounter cases where the output of one schema does not perfectly match the input of the next, for example, if you have `Schema` and `Schema` where `C` differs from `B`. To handle these cases, you can use the `{ strict: false }` option to relax type constraints. **Example** (Using Non-strict Option in Composition) ```ts twoslash import { Schema } from "effect" // Without the `strict: false` option, // this composition would raise a TypeScript error Schema.compose( // @ts-expect-error: Type mismatch between schemas Schema.Union(Schema.Null, Schema.Literal("0")), Schema.NumberFromString ) // Using `strict: false` to allow for type flexibility Schema.compose( Schema.Union(Schema.Null, Schema.Literal("0")), Schema.NumberFromString, { strict: false } ) ``` ## Effectful Filters The `Schema.filterEffect` function enables validations that require asynchronous or dynamic scenarios, making it suitable for cases where validations involve side effects like network requests or database queries. For simple synchronous validations, see [`Schema.filter`](/docs/schema/filters/#declaring-filters). **Example** (Asynchronous Username Validation) ```ts twoslash import { Effect, Schema } from "effect" // Mock async function to validate a username async function validateUsername(username: string) { return Promise.resolve(username === "gcanti") } // Define a schema with an effectful filter const ValidUsername = Schema.String.pipe( Schema.filterEffect((username) => Effect.promise(() => // Validate the username asynchronously, // returning an error message if invalid validateUsername(username).then( (valid) => valid || "Invalid username" ) ) ) ).annotations({ identifier: "ValidUsername" }) Effect.runPromise(Schema.decodeUnknown(ValidUsername)("xxx")).then( console.log ) /* ParseError: ValidUsername └─ Transformation process failure └─ Invalid username */ ``` ## String Transformations ### split Splits a string by a specified delimiter into an array of substrings. **Example** (Splitting a String by Comma) ```ts twoslash import { Schema } from "effect" const schema = Schema.split(",") const decode = Schema.decodeUnknownSync(schema) console.log(decode("")) // [""] console.log(decode(",")) // ["", ""] console.log(decode("a,")) // ["a", ""] console.log(decode("a,b")) // ["a", "b"] ``` ### Trim Removes whitespace from the beginning and end of a string. **Example** (Trimming Whitespace) ```ts twoslash import { Schema } from "effect" const decode = Schema.decodeUnknownSync(Schema.Trim) console.log(decode("a")) // "a" console.log(decode(" a")) // "a" console.log(decode("a ")) // "a" console.log(decode(" a ")) // "a" ``` ### Lowercase Converts a string to lowercase. **Example** (Converting to Lowercase) ```ts twoslash import { Schema } from "effect" const decode = Schema.decodeUnknownSync(Schema.Lowercase) console.log(decode("A")) // "a" console.log(decode(" AB")) // " ab" console.log(decode("Ab ")) // "ab " console.log(decode(" ABc ")) // " abc " ``` ### Uppercase Converts a string to uppercase. **Example** (Converting to Uppercase) ```ts twoslash import { Schema } from "effect" const decode = Schema.decodeUnknownSync(Schema.Uppercase) console.log(decode("a")) // "A" console.log(decode(" ab")) // " AB" console.log(decode("aB ")) // "AB " console.log(decode(" abC ")) // " ABC " ``` ### Capitalize Converts the first character of a string to uppercase. **Example** (Capitalizing a String) ```ts twoslash import { Schema } from "effect" const decode = Schema.decodeUnknownSync(Schema.Capitalize) console.log(decode("aa")) // "Aa" console.log(decode(" ab")) // " ab" console.log(decode("aB ")) // "AB " console.log(decode(" abC ")) // " abC " ``` ### Uncapitalize Converts the first character of a string to lowercase. **Example** (Uncapitalizing a String) ```ts twoslash import { Schema } from "effect" const decode = Schema.decodeUnknownSync(Schema.Uncapitalize) console.log(decode("AA")) // "aA" console.log(decode(" AB")) // " AB" console.log(decode("Ab ")) // "ab " console.log(decode(" AbC ")) // " AbC " ``` ### parseJson The `Schema.parseJson` constructor offers a method to convert JSON strings into the `unknown` type using the underlying functionality of `JSON.parse`. It also employs `JSON.stringify` for encoding. **Example** (Parsing JSON Strings) ```ts twoslash import { Schema } from "effect" const schema = Schema.parseJson() const decode = Schema.decodeUnknownSync(schema) // Parse valid JSON strings console.log(decode("{}")) // Output: {} console.log(decode(`{"a":"b"}`)) // Output: { a: "b" } // Attempting to decode an empty string results in an error decode("") /* throws: ParseError: (JsonString <-> unknown) └─ Transformation process failure └─ Unexpected end of JSON input */ ``` To further refine the result of JSON parsing, you can provide a schema to the `Schema.parseJson` constructor. This schema will validate that the parsed JSON matches a specific structure. **Example** (Parsing JSON with Structured Validation) In this example, `Schema.parseJson` uses a struct schema to ensure the parsed JSON is an object with a numeric property `a`. This adds validation to the parsed data, confirming that it follows the expected structure. ```ts twoslash import { Schema } from "effect" // ┌─── SchemaClass<{ readonly a: number; }, string, never> // ▼ const schema = Schema.parseJson(Schema.Struct({ a: Schema.Number })) ``` ### StringFromBase64 Decodes a base64 (RFC4648) encoded string into a UTF-8 string. **Example** (Decoding Base64) ```ts twoslash import { Schema } from "effect" const decode = Schema.decodeUnknownSync(Schema.StringFromBase64) console.log(decode("Zm9vYmFy")) // Output: "foobar" ``` ### StringFromBase64Url Decodes a base64 (URL) encoded string into a UTF-8 string. **Example** (Decoding Base64 URL) ```ts twoslash import { Schema } from "effect" const decode = Schema.decodeUnknownSync(Schema.StringFromBase64Url) console.log(decode("Zm9vYmFy")) // Output: "foobar" ``` ### StringFromHex Decodes a hex encoded string into a UTF-8 string. **Example** (Decoding Hex String) ```ts twoslash import { Schema } from "effect" const decode = Schema.decodeUnknownSync(Schema.StringFromHex) console.log(new TextEncoder().encode(decode("0001020304050607"))) /* Output: Uint8Array(8) [ 0, 1, 2, 3, 4, 5, 6, 7 ] */ ``` ### StringFromUriComponent Decodes a URI-encoded string into a UTF-8 string. It is useful for encoding and decoding data in URLs. **Example** (Decoding URI Component) ```ts twoslash import { Schema } from "effect" const PaginationSchema = Schema.Struct({ maxItemPerPage: Schema.Number, page: Schema.Number }) const UrlSchema = Schema.compose( Schema.StringFromUriComponent, Schema.parseJson(PaginationSchema) ) console.log(Schema.encodeSync(UrlSchema)({ maxItemPerPage: 10, page: 1 })) // Output: %7B%22maxItemPerPage%22%3A10%2C%22page%22%3A1%7D ``` ## Number Transformations ### NumberFromString Converts a string to a number using `parseFloat`, supporting special values "NaN", "Infinity", and "-Infinity". **Example** (Parsing Number from String) ```ts twoslash import { Schema } from "effect" const schema = Schema.NumberFromString const decode = Schema.decodeUnknownSync(schema) // success cases console.log(decode("1")) // 1 console.log(decode("-1")) // -1 console.log(decode("1.5")) // 1.5 console.log(decode("NaN")) // NaN console.log(decode("Infinity")) // Infinity console.log(decode("-Infinity")) // -Infinity // failure cases decode("a") /* throws: ParseError: NumberFromString └─ Transformation process failure └─ Expected NumberFromString, actual "a" */ ``` ### clamp Restricts a number within a specified range. **Example** (Clamping a Number) ```ts twoslash import { Schema } from "effect" // clamps the input to -1 <= x <= 1 const schema = Schema.Number.pipe(Schema.clamp(-1, 1)) const decode = Schema.decodeUnknownSync(schema) console.log(decode(-3)) // -1 console.log(decode(0)) // 0 console.log(decode(3)) // 1 ``` ### parseNumber Transforms a string into a number by parsing the string using the `parse` function of the `effect/Number` module. It returns an error if the value can't be converted (for example when non-numeric characters are provided). The following special string values are supported: "NaN", "Infinity", "-Infinity". **Example** (Parsing and Validating Numbers) ```ts twoslash import { Schema } from "effect" const schema = Schema.String.pipe(Schema.parseNumber) const decode = Schema.decodeUnknownSync(schema) console.log(decode("1")) // 1 console.log(decode("Infinity")) // Infinity console.log(decode("NaN")) // NaN console.log(decode("-")) /* throws ParseError: (string <-> number) └─ Transformation process failure └─ Expected (string <-> number), actual "-" */ ``` ## Boolean Transformations ### Not Negates a boolean value. **Example** (Negating Boolean) ```ts twoslash import { Schema } from "effect" const decode = Schema.decodeUnknownSync(Schema.Not) console.log(decode(true)) // false console.log(decode(false)) // true ``` ## Symbol transformations ### Symbol Converts a string to a symbol using `Symbol.for`. **Example** (Creating Symbols from Strings) ```ts twoslash import { Schema } from "effect" const decode = Schema.decodeUnknownSync(Schema.Symbol) console.log(decode("a")) // Symbol(a) ``` ## BigInt transformations ### BigInt Converts a string to a `BigInt` using the `BigInt` constructor. **Example** (Parsing BigInt from String) ```ts twoslash import { Schema } from "effect" const decode = Schema.decodeUnknownSync(Schema.BigInt) // success cases console.log(decode("1")) // 1n console.log(decode("-1")) // -1n // failure cases decode("a") /* throws: ParseError: bigint └─ Transformation process failure └─ Expected bigint, actual "a" */ decode("1.5") // throws decode("NaN") // throws decode("Infinity") // throws decode("-Infinity") // throws ``` ### BigIntFromNumber Converts a number to a `BigInt` using the `BigInt` constructor. **Example** (Parsing BigInt from Number) ```ts twoslash import { Schema } from "effect" const decode = Schema.decodeUnknownSync(Schema.BigIntFromNumber) const encode = Schema.encodeSync(Schema.BigIntFromNumber) // success cases console.log(decode(1)) // 1n console.log(decode(-1)) // -1n console.log(encode(1n)) // 1 console.log(encode(-1n)) // -1 // failure cases decode(1.5) /* throws: ParseError: BigintFromNumber └─ Transformation process failure └─ Expected BigintFromNumber, actual 1.5 */ decode(NaN) // throws decode(Infinity) // throws decode(-Infinity) // throws encode(BigInt(Number.MAX_SAFE_INTEGER) + 1n) // throws encode(BigInt(Number.MIN_SAFE_INTEGER) - 1n) // throws ``` ### clampBigInt Restricts a `BigInt` within a specified range. **Example** (Clamping BigInt) ```ts twoslash import { Schema } from "effect" // clamps the input to -1n <= x <= 1n const schema = Schema.BigIntFromSelf.pipe(Schema.clampBigInt(-1n, 1n)) const decode = Schema.decodeUnknownSync(schema) console.log(decode(-3n)) // Output: -1n console.log(decode(0n)) // Output: 0n console.log(decode(3n)) // Output: 1n ``` ## Date transformations ### Date Converts a string into a **valid** `Date`, ensuring that invalid dates, such as `new Date("Invalid Date")`, are rejected. **Example** (Parsing and Validating Date) ```ts twoslash import { Schema } from "effect" const decode = Schema.decodeUnknownSync(Schema.Date) console.log(decode("1970-01-01T00:00:00.000Z")) // Output: 1970-01-01T00:00:00.000Z decode("a") /* throws: ParseError: Date └─ Predicate refinement failure └─ Expected Date, actual Invalid Date */ const validate = Schema.validateSync(Schema.Date) console.log(validate(new Date(0))) // Output: 1970-01-01T00:00:00.000Z console.log(validate(new Date("Invalid Date"))) /* throws: ParseError: Date └─ Predicate refinement failure └─ Expected Date, actual Invalid Date */ ``` ## BigDecimal Transformations ### BigDecimal Converts a string to a `BigDecimal`. **Example** (Parsing BigDecimal from String) ```ts twoslash import { Schema } from "effect" const decode = Schema.decodeUnknownSync(Schema.BigDecimal) console.log(decode(".124")) // Output: { _id: 'BigDecimal', value: '124', scale: 3 } ``` ### BigDecimalFromNumber Converts a number to a `BigDecimal`. **Example** (Parsing BigDecimal from Number) ```ts twoslash import { Schema } from "effect" const decode = Schema.decodeUnknownSync(Schema.BigDecimalFromNumber) console.log(decode(0.111)) // Output: { _id: 'BigDecimal', value: '111', scale: 3 } ``` ### clampBigDecimal Clamps a `BigDecimal` within a specified range. **Example** (Clamping BigDecimal) ```ts twoslash import { Schema } from "effect" import { BigDecimal } from "effect" const schema = Schema.BigDecimal.pipe( Schema.clampBigDecimal( BigDecimal.fromNumber(-1), BigDecimal.fromNumber(1) ) ) const decode = Schema.decodeUnknownSync(schema) console.log(decode("-2")) // Output: { _id: 'BigDecimal', value: '-1', scale: 0 } console.log(decode("0")) // Output: { _id: 'BigDecimal', value: '0', scale: 0 } console.log(decode("3")) // Output: { _id: 'BigDecimal', value: '1', scale: 0 } ``` # [Creating Sinks](https://effect.website/docs/sink/creating/) ## Overview In stream processing, sinks are used to consume and handle elements from a stream. Here, we'll explore various sink constructors that allow you to create sinks for specific tasks. ## Common Constructors ### head The `Sink.head` sink retrieves only the first element from a stream, wrapping it in `Some`. If the stream has no elements, it returns `None`. **Example** (Retrieving the First Element) ```ts twoslash import { Stream, Sink, Effect } from "effect" const nonEmptyStream = Stream.make(1, 2, 3, 4) Effect.runPromise(Stream.run(nonEmptyStream, Sink.head())).then( console.log ) /* Output: { _id: 'Option', _tag: 'Some', value: 1 } */ const emptyStream = Stream.empty Effect.runPromise(Stream.run(emptyStream, Sink.head())).then(console.log) /* Output: { _id: 'Option', _tag: 'None' } */ ``` ### last The `Sink.last` sink retrieves only the last element from a stream, wrapping it in `Some`. If the stream has no elements, it returns `None`. **Example** (Retrieving the Last Element) ```ts twoslash import { Stream, Sink, Effect } from "effect" const nonEmptyStream = Stream.make(1, 2, 3, 4) Effect.runPromise(Stream.run(nonEmptyStream, Sink.last())).then( console.log ) /* Output: { _id: 'Option', _tag: 'Some', value: 4 } */ const emptyStream = Stream.empty Effect.runPromise(Stream.run(emptyStream, Sink.last())).then(console.log) /* Output: { _id: 'Option', _tag: 'None' } */ ``` ### count The `Sink.count` sink consumes all elements of the stream and counts the number of elements fed to it. ```ts twoslash import { Stream, Sink, Effect } from "effect" const stream = Stream.make(1, 2, 3, 4) Effect.runPromise(Stream.run(stream, Sink.count)).then(console.log) // Output: 4 ``` ### sum The `Sink.sum` sink consumes all elements of the stream and sums incoming numeric values. ```ts twoslash import { Stream, Sink, Effect } from "effect" const stream = Stream.make(1, 2, 3, 4) Effect.runPromise(Stream.run(stream, Sink.sum)).then(console.log) // Output: 10 ``` ### take The `Sink.take` sink takes the specified number of values from the stream and results in a [Chunk](/docs/data-types/chunk/) data type. ```ts twoslash import { Stream, Sink, Effect } from "effect" const stream = Stream.make(1, 2, 3, 4) Effect.runPromise(Stream.run(stream, Sink.take(3))).then(console.log) /* Output: { _id: 'Chunk', values: [ 1, 2, 3 ] } */ ``` ### drain The `Sink.drain` sink ignores its inputs, effectively discarding them. ```ts twoslash import { Stream, Console, Sink, Effect } from "effect" const stream = Stream.make(1, 2, 3, 4).pipe(Stream.tap(Console.log)) Effect.runPromise(Stream.run(stream, Sink.drain)).then(console.log) /* Output: 1 2 3 4 undefined */ ``` ### timed The `Sink.timed` sink executes the stream and measures its execution time, providing the [Duration](/docs/data-types/duration/). ```ts twoslash import { Stream, Schedule, Sink, Effect } from "effect" const stream = Stream.make(1, 2, 3, 4).pipe( Stream.schedule(Schedule.spaced("100 millis")) ) Effect.runPromise(Stream.run(stream, Sink.timed)).then(console.log) /* Output: { _id: 'Duration', _tag: 'Millis', millis: 408 } */ ``` ### forEach The `Sink.forEach` sink executes the provided effectful function for every element fed to it. ```ts twoslash import { Stream, Console, Sink, Effect } from "effect" const stream = Stream.make(1, 2, 3, 4) Effect.runPromise(Stream.run(stream, Sink.forEach(Console.log))).then( console.log ) /* Output: 1 2 3 4 undefined */ ``` ## Creating Sinks from Success and Failure Just as you can define streams to hold or manipulate data, you can also create sinks with specific success or failure outcomes using the `Sink.fail` and `Sink.succeed` functions. ### Succeeding Sink This example creates a sink that doesn’t consume any elements from its upstream source but instead immediately succeeds with a specified numeric value: **Example** (Sink that Always Succeeds with a Value) ```ts twoslash import { Stream, Sink, Effect } from "effect" const stream = Stream.make(1, 2, 3, 4) Effect.runPromise(Stream.run(stream, Sink.succeed(0))).then(console.log) // Output: 0 ``` ### Failing Sink In this example, the sink also doesn’t consume any elements from its upstream source. Instead, it fails with a specified error message of type `string`: **Example** (Sink that Always Fails with an Error Message) ```ts twoslash import { Stream, Sink, Effect } from "effect" const stream = Stream.make(1, 2, 3, 4) Effect.runPromiseExit(Stream.run(stream, Sink.fail("fail!"))).then( console.log ) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'fail!' } } */ ``` ## Collecting ### Collecting All Elements To gather all elements from a data stream into a [Chunk](/docs/data-types/chunk/), use the `Sink.collectAll` sink. The final output is a chunk containing all elements from the stream, in the order they were emitted. **Example** (Collecting All Stream Elements) ```ts twoslash import { Stream, Sink, Effect } from "effect" const stream = Stream.make(1, 2, 3, 4) Effect.runPromise(Stream.run(stream, Sink.collectAll())).then(console.log) /* Output: { _id: 'Chunk', values: [ 1, 2, 3, 4 ] } */ ``` ### Collecting a Specified Number To collect a fixed number of elements from a stream into a [Chunk](/docs/data-types/chunk/), use `Sink.collectAllN`. This sink stops collecting once it reaches the specified limit. **Example** (Collecting a Limited Number of Elements) ```ts twoslash import { Stream, Sink, Effect } from "effect" const stream = Stream.make(1, 2, 3, 4, 5) Effect.runPromise( Stream.run( stream, // Collect the first 3 elements into a Chunk Sink.collectAllN(3) ) ).then(console.log) /* Output: { _id: 'Chunk', values: [ 1, 2, 3 ] } */ ``` ### Collecting While Meeting a Condition To gather elements from a stream while they satisfy a specific condition, use `Sink.collectAllWhile`. This sink collects elements until the provided predicate returns `false`. **Example** (Collecting Elements Until a Condition Fails) ```ts twoslash import { Stream, Sink, Effect } from "effect" const stream = Stream.make(1, 2, 0, 4, 0, 6, 7) Effect.runPromise( Stream.run( stream, // Collect elements while they are not equal to 0 Sink.collectAllWhile((n) => n !== 0) ) ).then(console.log) /* Output: { _id: 'Chunk', values: [ 1, 2 ] } */ ``` ### Collecting into a HashSet To accumulate stream elements into a `HashSet`, use `Sink.collectAllToSet()`. This ensures that each element appears only once in the final set. **Example** (Collecting Unique Elements into a HashSet) ```ts twoslash import { Stream, Sink, Effect } from "effect" const stream = Stream.make(1, 2, 2, 3, 4, 4) Effect.runPromise(Stream.run(stream, Sink.collectAllToSet())).then( console.log ) /* Output: { _id: 'HashSet', values: [ 1, 2, 3, 4 ] } */ ``` ### Collecting into HashSets of a Specific Size For controlled collection into a `HashSet` with a specified maximum size, use `Sink.collectAllToSetN`. This sink gathers unique elements up to the given limit. **Example** (Collecting Unique Elements with a Set Size Limit) ```ts twoslash import { Stream, Sink, Effect } from "effect" const stream = Stream.make(1, 2, 2, 3, 4, 4) Effect.runPromise( Stream.run( stream, // Collect unique elements, limiting the set size to 3 Sink.collectAllToSetN(3) ) ).then(console.log) /* Output: { _id: 'HashSet', values: [ 1, 2, 3 ] } */ ``` ### Collecting into a HashMap For more complex collection scenarios, `Sink.collectAllToMap` lets you gather elements into a `HashMap` with a specified keying and merging strategy. This sink requires both a key function to define each element's grouping and a merge function to combine values sharing the same key. **Example** (Grouping and Merging Stream Elements in a HashMap) In this example, we use `(n) => n % 3` to determine map keys and `(a, b) => a + b` to merge elements with the same key: ```ts twoslash import { Stream, Sink, Effect } from "effect" const stream = Stream.make(1, 3, 2, 3, 1, 5, 1) Effect.runPromise( Stream.run( stream, Sink.collectAllToMap( (n) => n % 3, // Key function to group by element value (a, b) => a + b // Merge function to sum values with the same key ) ) ).then(console.log) /* Output: { _id: 'HashMap', values: [ [ 0, 6 ], [ 1, 3 ], [ 2, 7 ] ] } */ ``` ### Collecting into a HashMap with Limited Keys To accumulate elements into a `HashMap` with a maximum number of keys, use `Sink.collectAllToMapN`. This sink collects elements until it reaches the specified key limit, requiring a key function to define the grouping of each element and a merge function to combine values with the same key. **Example** (Limiting Collected Keys in a HashMap) ```ts twoslash import { Stream, Sink, Effect } from "effect" const stream = Stream.make(1, 3, 2, 3, 1, 5, 1) Effect.runPromise( Stream.run( stream, Sink.collectAllToMapN( 3, // Maximum of 3 keys (n) => n, // Key function to group by element value (a, b) => a + b // Merge function to sum values with the same key ) ) ).then(console.log) /* Output: { _id: 'HashMap', values: [ [ 1, 2 ], [ 2, 2 ], [ 3, 6 ] ] } */ ``` ## Folding ### Folding Left If you want to reduce a stream into a single cumulative value by applying an operation to each element in sequence, you can use the `Sink.foldLeft` function. **Example** (Summing Elements in a Stream Using Fold Left) ```ts twoslash import { Stream, Sink, Effect } from "effect" const stream = Stream.make(1, 2, 3, 4) Effect.runPromise( Stream.run( stream, // Use foldLeft to sequentially add each element, starting with 0 Sink.foldLeft(0, (a, b) => a + b) ) ).then(console.log) // Output: 10 ``` ### Folding with Termination Sometimes, you may want to fold elements in a stream but stop the process once a specific condition is met. This is known as "short-circuiting." You can accomplish this with the `Sink.fold` function, which lets you define a termination condition. **Example** (Folding with a Condition to Stop Early) ```ts twoslash import { Stream, Sink, Effect } from "effect" const stream = Stream.iterate(0, (n) => n + 1) Effect.runPromise( Stream.run( stream, Sink.fold( 0, // Initial value (sum) => sum <= 10, // Termination condition (a, b) => a + b // Folding operation ) ) ).then(console.log) // Output: 15 ``` ### Folding Until a Limit To accumulate elements until a specific count is reached, use `Sink.foldUntil`. This sink folds elements up to the specified limit and then stops. **Example** (Accumulating a Set Number of Elements) ```ts twoslash import { Stream, Sink, Effect } from "effect" const stream = Stream.make(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) Effect.runPromise( Stream.run( stream, // Fold elements, stopping after accumulating 3 values Sink.foldUntil(0, 3, (a, b) => a + b) ) ).then(console.log) // Output: 6 ``` ### Folding with Weighted Elements In some scenarios, you may want to fold elements based on a defined "weight" or "cost," accumulating elements until a specified maximum cost is reached. You can accomplish this with `Sink.foldWeighted`. **Example** (Accumulating Elements Based on Weight) In the example below, each element has a weight of `1`, and the folding resets when the accumulated weight hits `3`. ```ts twoslash import { Stream, Sink, Chunk, Effect } from "effect" const stream = Stream.make(3, 2, 4, 1, 5, 6, 2, 1, 3, 5, 6).pipe( Stream.transduce( Sink.foldWeighted({ initial: Chunk.empty(), // Initial empty Chunk maxCost: 3, // Maximum accumulated cost cost: () => 1, // Each element has a weight of 1 body: (acc, el) => Chunk.append(acc, el) // Append element to the Chunk }) ) ) Effect.runPromise(Stream.runCollect(stream)).then((chunk) => console.log("%o", chunk) ) /* Output: { _id: 'Chunk', values: [ { _id: 'Chunk', values: [ 3, 2, 4, [length]: 3 ] }, { _id: 'Chunk', values: [ 1, 5, 6, [length]: 3 ] }, { _id: 'Chunk', values: [ 2, 1, 3, [length]: 3 ] }, { _id: 'Chunk', values: [ 5, 6, [length]: 2 ] }, [length]: 4 ] } */ ``` # [Introduction](https://effect.website/docs/sink/introduction/) ## Overview In stream processing, a `Sink` is a construct designed to consume elements generated by a `Stream`. ```text showLineNumbers=false ┌─── Type of the result produced by the Sink | ┌─── Type of elements consumed by the Sink | | ┌─── Type of any leftover elements │ | | ┌─── Type of possible errors │ │ | | ┌─── Type of required dependencies ▼ ▼ ▼ ▼ ▼ Sink ``` Here's an overview of what a `Sink` does: - It consumes a varying number of `In` elements, which may include zero, one, or multiple elements. - It can encounter errors of type `E` during processing. - It produces a result of type `A` once processing completes. - It can also return a remainder of type `L`, representing any leftover elements. To process a stream using a `Sink`, you can pass it directly to the `Stream.run` function: **Example** (Using a Sink to Collect Stream Elements) ```ts twoslash import { Stream, Sink, Effect } from "effect" // ┌─── Stream // ▼ const stream = Stream.make(1, 2, 3) // Create a sink to take the first 2 elements of the stream // // ┌─── Sink, number, number, never, never> // ▼ const sink = Sink.take(2) // Run the stream through the sink to collect the elements // // ┌─── Effect // ▼ const sum = Stream.run(stream, sink) Effect.runPromise(sum).then(console.log) /* Output: { _id: 'Chunk', values: [ 1, 2 ] } */ ``` The type of `sink` is as follows: ```text showLineNumbers=false ┌─── result | ┌─── consumed elements | | ┌─── leftover elements │ | | ┌─── no errors │ │ | | ┌─── no dependencies ▼ ▼ ▼ ▼ ▼ Sink, number, number, never, never> ``` Here's the breakdown: - `Chunk`: The final result produced by the sink after processing elements (in this case, a [Chunk](/docs/data-types/chunk/) of numbers). - `number` (first occurrence): The type of elements that the sink will consume from the stream. - `number` (second occurrence): The type of leftover elements, if any, that are not consumed. - `never` (first occurrence): Indicates that this sink does not produce any errors. - `never` (second occurrence): Shows that no dependencies are required to operate this sink. # [Leftovers](https://effect.website/docs/sink/leftovers/) ## Overview In this section, we'll look at handling elements left unconsumed by sinks. Sinks may process only a portion of the elements from an upstream source, leaving some elements as "leftovers." Here's how to collect or ignore these remaining elements. ## Collecting Leftovers If a sink doesn't consume all elements from the upstream source, the remaining elements are called leftovers. To capture these leftovers, use `Sink.collectLeftover`, which returns a tuple containing the result of the sink operation and any unconsumed elements. **Example** (Collecting Leftover Elements) ```ts twoslash import { Stream, Sink, Effect } from "effect" const stream = Stream.make(1, 2, 3, 4, 5) // Take the first 3 elements and collect any leftovers const sink1 = Sink.take(3).pipe(Sink.collectLeftover) Effect.runPromise(Stream.run(stream, sink1)).then(console.log) /* Output: [ { _id: 'Chunk', values: [ 1, 2, 3 ] }, { _id: 'Chunk', values: [ 4, 5 ] } ] */ // Take only the first element and collect the rest as leftovers const sink2 = Sink.head().pipe(Sink.collectLeftover) Effect.runPromise(Stream.run(stream, sink2)).then(console.log) /* Output: [ { _id: 'Option', _tag: 'Some', value: 1 }, { _id: 'Chunk', values: [ 2, 3, 4, 5 ] } ] */ ``` ## Ignoring Leftovers If leftover elements are not needed, you can ignore them using `Sink.ignoreLeftover`. This approach discards any unconsumed elements, so the sink operation focuses only on the elements it needs. **Example** (Ignoring Leftover Elements) ```ts twoslash import { Stream, Sink, Effect } from "effect" const stream = Stream.make(1, 2, 3, 4, 5) // Take the first 3 elements and ignore any remaining elements const sink = Sink.take(3).pipe( Sink.ignoreLeftover, Sink.collectLeftover ) Effect.runPromise(Stream.run(stream, sink)).then(console.log) /* Output: [ { _id: 'Chunk', values: [ 1, 2, 3 ] }, { _id: 'Chunk', values: [] } ] */ ``` # [Sink Operations](https://effect.website/docs/sink/operations/) ## Overview In previous sections, we learned how to create and use sinks. Now, let's explore some operations that let you transform or filter sink behavior. ## Adapting Sink Input At times, you may have a sink that works with one type of input, but your current stream uses a different type. The `Sink.mapInput` function helps you adapt your sink to a new input type by transforming the input values. While `Sink.map` changes the sink's output, `Sink.mapInput` changes the input it accepts. **Example** (Converting String Input to Numeric for Summing) Suppose you have a `Sink.sum` that calculates the sum of numbers. If your stream contains strings rather than numbers, `Sink.mapInput` can convert those strings into numbers, allowing `Sink.sum` to work with your stream: ```ts twoslash import { Stream, Sink, Effect } from "effect" // A stream of numeric strings const stream = Stream.make("1", "2", "3", "4", "5") // Define a sink for summing numeric values const numericSum = Sink.sum // Use mapInput to adapt the sink, converting strings to numbers const stringSum = numericSum.pipe( Sink.mapInput((s: string) => Number.parseFloat(s)) ) Effect.runPromise(Stream.run(stream, stringSum)).then(console.log) // Output: 15 ``` ## Transforming Both Input and Output When you need to transform both the input and output of a sink, `Sink.dimap` provides a flexible solution. It extends `mapInput` by allowing you to transform the input type, perform the operation, and then transform the output to a new type. This can be useful for complete conversions between input and output types. **Example** (Converting Input to Integer, Summing, and Converting Output to String) ```ts twoslash import { Stream, Sink, Effect } from "effect" // A stream of numeric strings const stream = Stream.make("1", "2", "3", "4", "5") // Convert string inputs to numbers, sum them, // then convert the result to a string const sumSink = Sink.dimap(Sink.sum, { // Transform input: string to number onInput: (s: string) => Number.parseFloat(s), // Transform output: number to string onDone: (n) => String(n) }) Effect.runPromise(Stream.run(stream, sumSink)).then(console.log) // Output: "15" ``` ## Filtering Input Sinks can also filter incoming elements based on specific conditions with `Sink.filterInput`. This operation allows the sink to process only elements that meet certain criteria. **Example** (Filtering Negative Numbers in Chunks of Three) In the example below, elements are collected in chunks of three, but only positive numbers are included: ```ts twoslash import { Stream, Sink, Effect } from "effect" // Define a stream with positive, negative, and zero values const stream = Stream.fromIterable([ 1, -2, 0, 1, 3, -3, 4, 2, 0, 1, -3, 1, 1, 6 ]).pipe( Stream.transduce( // Collect chunks of 3, filtering out non-positive numbers Sink.collectAllN(3).pipe(Sink.filterInput((n) => n > 0)) ) ) Effect.runPromise(Stream.runCollect(stream)).then((chunk) => console.log("%o", chunk) ) /* Output: { _id: 'Chunk', values: [ { _id: 'Chunk', values: [ 1, 1, 3, [length]: 3 ] }, { _id: 'Chunk', values: [ 4, 2, 1, [length]: 3 ] }, { _id: 'Chunk', values: [ 1, 1, 6, [length]: 3 ] }, { _id: 'Chunk', values: [ [length]: 0 ] }, [length]: 4 ] } */ ``` # [Ref](https://effect.website/docs/state-management/ref/) ## Overview import { Aside } from "@astrojs/starlight/components" When we write programs, it is common to need to keep track of some form of state during the execution of the program. State refers to any data that can change as the program runs. For example, in a counter application, the count value changes as the user increments or decrements it. Similarly, in a banking application, the account balance changes as deposits and withdrawals are made. State management is crucial to building interactive and dynamic applications. In traditional imperative programming, one common way to store state is using variables. However, this approach can introduce bugs, especially when the state is shared between multiple components or functions. As the program becomes more complex, managing shared state can become challenging. To overcome these issues, Effect introduces a powerful data type called `Ref`, which represents a mutable reference. With `Ref`, we can share state between different parts of our program without relying on mutable variables directly. Instead, `Ref` provides a controlled way to handle mutable state and safely update it in a concurrent environment. Effect's `Ref` data type enables communication between different fibers in your program. This capability is crucial in concurrent programming, where multiple tasks may need to access and update shared state simultaneously. In this guide, we will explore how to use the `Ref` data type to manage state in your programs effectively. We will cover simple examples like counting, as well as more complex scenarios where state is shared between different parts of the program. Additionally, we will show how to use `Ref` in a concurrent environment, allowing multiple tasks to interact with shared state safely. Let's dive in and see how we can leverage `Ref` for effective state management in your Effect programs. ## Using Ref Here is a simple example using `Ref` to create a counter: **Example** (Basic Counter with `Ref`) ```ts twoslash import { Effect, Ref } from "effect" class Counter { inc: Effect.Effect dec: Effect.Effect get: Effect.Effect constructor(private value: Ref.Ref) { this.inc = Ref.update(this.value, (n) => n + 1) this.dec = Ref.update(this.value, (n) => n - 1) this.get = Ref.get(this.value) } } const make = Effect.andThen(Ref.make(0), (value) => new Counter(value)) ``` **Example** (Using the Counter) ```ts twoslash collapse={3-15} import { Effect, Ref } from "effect" class Counter { inc: Effect.Effect dec: Effect.Effect get: Effect.Effect constructor(private value: Ref.Ref) { this.inc = Ref.update(this.value, (n) => n + 1) this.dec = Ref.update(this.value, (n) => n - 1) this.get = Ref.get(this.value) } } const make = Effect.andThen(Ref.make(0), (value) => new Counter(value)) const program = Effect.gen(function* () { const counter = yield* make yield* counter.inc yield* counter.inc yield* counter.dec yield* counter.inc const value = yield* counter.get console.log(`This counter has a value of ${value}.`) }) Effect.runPromise(program) /* Output: This counter has a value of 2. */ ``` ## Using Ref in a Concurrent Environment We can also use `Ref` in concurrent scenarios, where multiple tasks might be updating shared state at the same time. **Example** (Concurrent Updates to Shared Counter) For this example, let's update the counter concurrently: ```ts twoslash collapse={3-15} import { Effect, Ref } from "effect" class Counter { inc: Effect.Effect dec: Effect.Effect get: Effect.Effect constructor(private value: Ref.Ref) { this.inc = Ref.update(this.value, (n) => n + 1) this.dec = Ref.update(this.value, (n) => n - 1) this.get = Ref.get(this.value) } } const make = Effect.andThen(Ref.make(0), (value) => new Counter(value)) const program = Effect.gen(function* () { const counter = yield* make // Helper to log the counter's value before running an effect const logCounter = ( label: string, effect: Effect.Effect ) => Effect.gen(function* () { const value = yield* counter.get yield* Effect.log(`${label} get: ${value}`) return yield* effect }) yield* logCounter("task 1", counter.inc).pipe( Effect.zip(logCounter("task 2", counter.inc), { concurrent: true }), Effect.zip(logCounter("task 3", counter.dec), { concurrent: true }), Effect.zip(logCounter("task 4", counter.inc), { concurrent: true }) ) const value = yield* counter.get yield* Effect.log(`This counter has a value of ${value}.`) }) Effect.runPromise(program) /* Output: timestamp=... fiber=#3 message="task 4 get: 0" timestamp=... fiber=#6 message="task 3 get: 1" timestamp=... fiber=#8 message="task 1 get: 0" timestamp=... fiber=#9 message="task 2 get: 1" timestamp=... fiber=#0 message="This counter has a value of 2." */ ``` ## Using Ref as a Service You can pass a `Ref` as a [service](/docs/requirements-management/services/) to share state across different parts of your program. **Example** (Using `Ref` as a Service) ```ts twoslash import { Effect, Context, Ref } from "effect" // Create a Tag for our state class MyState extends Context.Tag("MyState")< MyState, Ref.Ref >() {} // Subprogram 1: Increment the state value twice const subprogram1 = Effect.gen(function* () { const state = yield* MyState yield* Ref.update(state, (n) => n + 1) yield* Ref.update(state, (n) => n + 1) }) // Subprogram 2: Decrement the state value and then increment it const subprogram2 = Effect.gen(function* () { const state = yield* MyState yield* Ref.update(state, (n) => n - 1) yield* Ref.update(state, (n) => n + 1) }) // Subprogram 3: Read and log the current value of the state const subprogram3 = Effect.gen(function* () { const state = yield* MyState const value = yield* Ref.get(state) console.log(`MyState has a value of ${value}.`) }) // Compose subprograms 1, 2, and 3 to create the main program const program = Effect.gen(function* () { yield* subprogram1 yield* subprogram2 yield* subprogram3 }) // Create a Ref instance with an initial value of 0 const initialState = Ref.make(0) // Provide the Ref as a service const runnable = program.pipe( Effect.provideServiceEffect(MyState, initialState) ) // Run the program and observe the output Effect.runPromise(runnable) /* Output: MyState has a value of 2. */ ``` Note that we use `Effect.provideServiceEffect` instead of `Effect.provideService` to provide an actual implementation of the `MyState` service because all the operations on the `Ref` data type are effectful, including the creation `Ref.make(0)`. ## Sharing State Between Fibers You can use `Ref` to manage shared state between multiple fibers in a concurrent environment. **Example** (Managing Shared State Across Fibers) Let's look at an example where we continuously read names from user input until the user enters `"q"` to exit. First, let's introduce a `readLine` utility to read user input (ensure you have `@types/node` installed): ```ts twoslash import { Effect } from "effect" import * as NodeReadLine from "node:readline" // Utility to read user input const readLine = (message: string): Effect.Effect => Effect.promise( () => new Promise((resolve) => { const rl = NodeReadLine.createInterface({ input: process.stdin, output: process.stdout }) rl.question(message, (answer) => { rl.close() resolve(answer) }) }) ) ``` Next, we implement the main program to collect names: ```ts twoslash collapse={5-18} import { Effect, Chunk, Ref } from "effect" import * as NodeReadLine from "node:readline" // Utility to read user input const readLine = (message: string): Effect.Effect => Effect.promise( () => new Promise((resolve) => { const rl = NodeReadLine.createInterface({ input: process.stdin, output: process.stdout }) rl.question(message, (answer) => { rl.close() resolve(answer) }) }) ) const getNames = Effect.gen(function* () { const ref = yield* Ref.make(Chunk.empty()) while (true) { const name = yield* readLine("Please enter a name or `q` to exit: ") if (name === "q") { break } yield* Ref.update(ref, (state) => Chunk.append(state, name)) } return yield* Ref.get(ref) }) Effect.runPromise(getNames).then(console.log) /* Output: Please enter a name or `q` to exit: Alice Please enter a name or `q` to exit: Bob Please enter a name or `q` to exit: q { _id: "Chunk", values: [ "Alice", "Bob" ] } */ ``` Now that we have learned how to use the `Ref` data type, we can use it to manage the state concurrently. For example, assume while we are reading from the console, we have another fiber that is trying to update the state from a different source. Here, one fiber reads names from user input, while another fiber concurrently adds preset names at regular intervals: ```ts twoslash collapse={5-18} import { Effect, Chunk, Ref, Fiber } from "effect" import * as NodeReadLine from "node:readline" // Utility to read user input const readLine = (message: string): Effect.Effect => Effect.promise( () => new Promise((resolve) => { const rl = NodeReadLine.createInterface({ input: process.stdin, output: process.stdout }) rl.question(message, (answer) => { rl.close() resolve(answer) }) }) ) const getNames = Effect.gen(function* () { const ref = yield* Ref.make(Chunk.empty()) // Fiber 1: Reading names from user input const fiber1 = yield* Effect.fork( Effect.gen(function* () { while (true) { const name = yield* readLine( "Please enter a name or `q` to exit: " ) if (name === "q") { break } yield* Ref.update(ref, (state) => Chunk.append(state, name)) } }) ) // Fiber 2: Updating the state with predefined names const fiber2 = yield* Effect.fork( Effect.gen(function* () { for (const name of ["John", "Jane", "Joe", "Tom"]) { yield* Ref.update(ref, (state) => Chunk.append(state, name)) yield* Effect.sleep("1 second") } }) ) yield* Fiber.join(fiber1) yield* Fiber.join(fiber2) return yield* Ref.get(ref) }) Effect.runPromise(getNames).then(console.log) /* Output: Please enter a name or `q` to exit: Alice Please enter a name or `q` to exit: Bob Please enter a name or `q` to exit: q { _id: "Chunk", // Note: the following result may vary // depending on the speed of user input values: [ 'John', 'Jane', 'Joe', 'Tom', 'Alice', 'Bob' ] } */ ``` # [SubscriptionRef](https://effect.website/docs/state-management/subscriptionref/) ## Overview A `SubscriptionRef` is a specialized form of a [SynchronizedRef](/docs/state-management/synchronizedref/). It allows us to subscribe and receive updates on the current value and any changes made to that value. ```ts showLineNumbers=false interface SubscriptionRef extends SynchronizedRef { /** * A stream containing the current value of the `Ref` as well as all changes * to that value. */ readonly changes: Stream } ``` You can perform all standard operations on a `SubscriptionRef`, such as `get`, `set`, or `modify` to interact with the current value. The key feature of `SubscriptionRef` is its `changes` stream. This stream allows you to observe the current value at the moment of subscription and receive all subsequent changes. Every time the stream is run, it emits the current value and tracks future updates. To create a `SubscriptionRef`, you can use the `SubscriptionRef.make` constructor, specifying the initial value: **Example** (Creating a `SubscriptionRef`) ```ts twoslash import { SubscriptionRef } from "effect" const ref = SubscriptionRef.make(0) ``` `SubscriptionRef` is particularly useful for modeling shared state when multiple observers need to react to changes. For example, in functional reactive programming, the `SubscriptionRef` could represent a portion of the application state, and various observers (like UI components) would update in response to state changes. **Example** (Server-Client Model with `SubscriptionRef`) In the following example, a "server" continually updates a shared value, while multiple "clients" observe the changes: ```ts twoslash import { Ref, Effect } from "effect" // Server function that increments a shared value forever const server = (ref: Ref.Ref) => Ref.update(ref, (n) => n + 1).pipe(Effect.forever) ``` The `server` function operates on a regular `Ref` and continuously updates the value. It doesn't need to know about `SubscriptionRef` directly. Next, let's define a `client` that subscribes to changes and collects a specified number of values: ```ts twoslash import { Ref, Effect, Stream, Random } from "effect" // Server function that increments a shared value forever const server = (ref: Ref.Ref) => Ref.update(ref, (n) => n + 1).pipe(Effect.forever) // Client function that observes the stream of changes const client = (changes: Stream.Stream) => Effect.gen(function* () { const n = yield* Random.nextIntBetween(1, 10) const chunk = yield* Stream.runCollect(Stream.take(changes, n)) return chunk }) ``` Similarly, the `client` function only works with a `Stream` of values and doesn't concern itself with the source of these values. To tie everything together, we start the server, launch multiple client instances in parallel, and then shut down the server when we're finished. We also create the `SubscriptionRef` in this process. ```ts twoslash import { Ref, Effect, Stream, Random, SubscriptionRef, Fiber } from "effect" // Server function that increments a shared value forever const server = (ref: Ref.Ref) => Ref.update(ref, (n) => n + 1).pipe(Effect.forever) // Client function that observes the stream of changes const client = (changes: Stream.Stream) => Effect.gen(function* () { const n = yield* Random.nextIntBetween(1, 10) const chunk = yield* Stream.runCollect(Stream.take(changes, n)) return chunk }) const program = Effect.gen(function* () { // Create a SubscriptionRef with an initial value of 0 const ref = yield* SubscriptionRef.make(0) // Fork the server to run concurrently const serverFiber = yield* Effect.fork(server(ref)) // Create 5 clients that subscribe to the changes stream const clients = new Array(5).fill(null).map(() => client(ref.changes)) // Run all clients in concurrently and collect their results const chunks = yield* Effect.all(clients, { concurrency: "unbounded" }) // Interrupt the server when clients are done yield* Fiber.interrupt(serverFiber) // Output the results collected by each client for (const chunk of chunks) { console.log(chunk) } }) Effect.runPromise(program) /* Example Output: { _id: 'Chunk', values: [ 4, 5, 6, 7, 8, 9 ] } { _id: 'Chunk', values: [ 4 ] } { _id: 'Chunk', values: [ 4, 5, 6, 7, 8, 9 ] } { _id: 'Chunk', values: [ 4, 5 ] } { _id: 'Chunk', values: [ 4, 5, 6, 7, 8, 9 ] } */ ``` This setup ensures that each client observes the current value when it starts and receives all subsequent changes to the value. Since the changes are represented as streams, you can easily build more complex programs using familiar stream operators. You can transform, filter, or merge these streams with other streams to achieve more sophisticated behavior. # [SynchronizedRef](https://effect.website/docs/state-management/synchronizedref/) ## Overview import { Aside } from "@astrojs/starlight/components" `SynchronizedRef` serves as a mutable reference to a value of type `A`. With it, we can store **immutable** data and perform updates **atomically** and effectfully. The distinctive function in `SynchronizedRef` is `updateEffect`. This function takes an effectful operation and executes it to modify the shared state. This is the key feature setting `SynchronizedRef` apart from `Ref`. In real-world applications, `SynchronizedRef` is useful when you need to execute effects, such as querying a database, and then update shared state based on the result. It ensures that updates happen sequentially, preserving consistency in concurrent environments. **Example** (Concurrent Updates with `SynchronizedRef`) In this example, we simulate fetching user ages concurrently and updating a shared state that stores the ages: ```ts twoslash import { Effect, SynchronizedRef } from "effect" // Simulated API to get user age const getUserAge = (userId: number) => Effect.succeed(userId * 10).pipe(Effect.delay(10 - userId)) const meanAge = Effect.gen(function* () { // Initialize a SynchronizedRef to hold an array of ages const ref = yield* SynchronizedRef.make([]) // Helper function to log state before each effect const log = (label: string, effect: Effect.Effect) => Effect.gen(function* () { const value = yield* SynchronizedRef.get(ref) yield* Effect.log(label, value) return yield* effect }) const task = (id: number) => log( `task ${id}`, SynchronizedRef.updateEffect(ref, (sumOfAges) => Effect.gen(function* () { const age = yield* getUserAge(id) return sumOfAges.concat(age) }) ) ) // Run tasks concurrently with a limit of 2 concurrent tasks yield* Effect.all([task(1), task(2), task(3), task(4)], { concurrency: 2 }) // Retrieve the updated value const value = yield* SynchronizedRef.get(ref) return value }) Effect.runPromise(meanAge).then(console.log) /* Output: timestamp=... level=INFO fiber=#2 message="task 1" message=[] timestamp=... level=INFO fiber=#3 message="task 2" message=[] timestamp=... level=INFO fiber=#2 message="task 3" message="[ 10 ]" timestamp=... level=INFO fiber=#3 message="task 4" message="[ 10, 20 ]" [ 10, 20, 30, 40 ] */ ``` # [Consuming Streams](https://effect.website/docs/stream/consuming-streams/) ## Overview When working with streams, it's essential to understand how to consume the data they produce. In this guide, we'll walk through several common methods for consuming streams. ## Using runCollect To gather all the elements from a stream into a single `Chunk`, you can use the `Stream.runCollect` function. ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.make(1, 2, 3, 4, 5) const collectedData = Stream.runCollect(stream) Effect.runPromise(collectedData).then(console.log) /* Output: { _id: "Chunk", values: [ 1, 2, 3, 4, 5 ] } */ ``` ## Using runForEach Another way to consume elements of a stream is by using `Stream.runForEach`. It takes a callback function that receives each element of the stream. Here's an example: ```ts twoslash import { Stream, Effect, Console } from "effect" const effect = Stream.make(1, 2, 3).pipe( Stream.runForEach((n) => Console.log(n)) ) Effect.runPromise(effect).then(console.log) /* Output: 1 2 3 undefined */ ``` In this example, we use `Stream.runForEach` to log each element to the console. ## Using a Fold Operation The `Stream.fold` function is another way to consume a stream by performing a fold operation over the stream of values and returning an effect containing the result. Here are a couple of examples: ```ts twoslash import { Stream, Effect } from "effect" const foldedStream = Stream.make(1, 2, 3, 4, 5).pipe( Stream.runFold(0, (a, b) => a + b) ) Effect.runPromise(foldedStream).then(console.log) // Output: 15 const foldedWhileStream = Stream.make(1, 2, 3, 4, 5).pipe( Stream.runFoldWhile( 0, (n) => n <= 3, (a, b) => a + b ) ) Effect.runPromise(foldedWhileStream).then(console.log) // Output: 6 ``` In the first example (`foldedStream`), we use `Stream.runFold` to calculate the sum of all elements. In the second example (`foldedWhileStream`), we use `Stream.runFoldWhile` to calculate the sum but only until a certain condition is met. ## Using a Sink To consume a stream using a Sink, you can pass the `Sink` to the `Stream.run` function. Here's an example: ```ts twoslash import { Stream, Sink, Effect } from "effect" const effect = Stream.make(1, 2, 3).pipe(Stream.run(Sink.sum)) Effect.runPromise(effect).then(console.log) // Output: 6 ``` In this example, we use a `Sink` to calculate the sum of the elements in the stream. # [Creating Streams](https://effect.website/docs/stream/creating/) ## Overview In this section, we'll explore various methods for creating Effect `Stream`s. These methods will help you generate streams tailored to your needs. ## Common Constructors ### make You can create a pure stream by using the `Stream.make` constructor. This constructor accepts a variable list of values as its arguments. ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.make(1, 2, 3) Effect.runPromise(Stream.runCollect(stream)).then(console.log) // { _id: 'Chunk', values: [ 1, 2, 3 ] } ``` ### empty Sometimes, you may require a stream that doesn't produce any values. In such cases, you can use `Stream.empty`. This constructor creates a stream that remains empty. ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.empty Effect.runPromise(Stream.runCollect(stream)).then(console.log) // { _id: 'Chunk', values: [] } ``` ### void If you need a stream that contains a single `void` value, you can use `Stream.void`. This constructor is handy when you want to represent a stream with a single event or signal. ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.void Effect.runPromise(Stream.runCollect(stream)).then(console.log) // { _id: 'Chunk', values: [ undefined ] } ``` ### range To create a stream of integers within a specified range `[min, max]` (including both endpoints, `min` and `max`), you can use `Stream.range`. This is particularly useful for generating a stream of sequential numbers. ```ts twoslash import { Stream, Effect } from "effect" // Creating a stream of numbers from 1 to 5 const stream = Stream.range(1, 5) Effect.runPromise(Stream.runCollect(stream)).then(console.log) // { _id: 'Chunk', values: [ 1, 2, 3, 4, 5 ] } ``` ### iterate With `Stream.iterate`, you can generate a stream by applying a function iteratively to an initial value. The initial value becomes the first element produced by the stream, followed by subsequent values produced by `f(init)`, `f(f(init))`, and so on. ```ts twoslash import { Stream, Effect } from "effect" // Creating a stream of incrementing numbers const stream = Stream.iterate(1, (n) => n + 1) // Produces 1, 2, 3, ... Effect.runPromise(Stream.runCollect(stream.pipe(Stream.take(5)))).then( console.log ) // { _id: 'Chunk', values: [ 1, 2, 3, 4, 5 ] } ``` ### scoped `Stream.scoped` is used to create a single-valued stream from a scoped resource. It can be handy when dealing with resources that require explicit acquisition, usage, and release. ```ts twoslash import { Stream, Effect, Console } from "effect" // Creating a single-valued stream from a scoped resource const stream = Stream.scoped( Effect.acquireUseRelease( Console.log("acquire"), () => Console.log("use"), () => Console.log("release") ) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: acquire use release { _id: 'Chunk', values: [ undefined ] } */ ``` ## From Success and Failure Much like the `Effect` data type, you can generate a `Stream` using the `fail` and `succeed` functions: ```ts twoslash import { Stream, Effect } from "effect" // Creating a stream that can emit errors const streamWithError: Stream.Stream = Stream.fail("Uh oh!") Effect.runPromise(Stream.runCollect(streamWithError)) // throws Error: Uh oh! // Creating a stream that emits a numeric value const streamWithNumber: Stream.Stream = Stream.succeed(5) Effect.runPromise(Stream.runCollect(streamWithNumber)).then(console.log) // { _id: 'Chunk', values: [ 5 ] } ``` ## From Chunks You can construct a stream from a `Chunk` like this: ```ts twoslash import { Stream, Chunk, Effect } from "effect" // Creating a stream with values from a single Chunk const stream = Stream.fromChunk(Chunk.make(1, 2, 3)) Effect.runPromise(Stream.runCollect(stream)).then(console.log) // { _id: 'Chunk', values: [ 1, 2, 3 ] } ``` Moreover, you can create a stream from multiple `Chunk`s as well: ```ts twoslash import { Stream, Chunk, Effect } from "effect" // Creating a stream with values from multiple Chunks const stream = Stream.fromChunks(Chunk.make(1, 2, 3), Chunk.make(4, 5, 6)) Effect.runPromise(Stream.runCollect(stream)).then(console.log) // { _id: 'Chunk', values: [ 1, 2, 3, 4, 5, 6 ] } ``` ## From Effect You can generate a stream from an Effect workflow by employing the `Stream.fromEffect` constructor. For instance, consider the following stream, which generates a single random number: ```ts twoslash import { Stream, Random, Effect } from "effect" const stream = Stream.fromEffect(Random.nextInt) Effect.runPromise(Stream.runCollect(stream)).then(console.log) // Example Output: { _id: 'Chunk', values: [ 1042302242 ] } ``` This method allows you to seamlessly transform the output of an Effect into a stream, providing a straightforward way to work with asynchronous operations within your streams. ## From Asynchronous Callback Imagine you have an asynchronous function that relies on callbacks. If you want to capture the results emitted by those callbacks as a stream, you can use the `Stream.async` function. This function is designed to adapt functions that invoke their callbacks multiple times and emit the results as a stream. Let's break down how to use it in the following example: ```ts twoslash import { Stream, Effect, Chunk, Option, StreamEmit } from "effect" const events = [1, 2, 3, 4] const stream = Stream.async( (emit: StreamEmit.Emit) => { events.forEach((n) => { setTimeout(() => { if (n === 3) { // Terminate the stream emit(Effect.fail(Option.none())) } else { // Add the current item to the stream emit(Effect.succeed(Chunk.of(n))) } }, 100 * n) }) } ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) // { _id: 'Chunk', values: [ 1, 2 ] } ``` The `StreamEmit.Emit` type represents an asynchronous callback that can be called multiple times. This callback takes a value of type `Effect, Option, R>`. Here's what each of the possible outcomes means: - When the value provided to the callback results in a `Chunk` upon success, it signifies that the specified elements should be emitted as part of the stream. - If the value passed to the callback results in a failure with `Some`, it indicates the termination of the stream with the specified error. - When the value passed to the callback results in a failure with `None`, it serves as a signal for the end of the stream, essentially terminating it. To put it simply, this type allows you to specify how your asynchronous callback interacts with the stream, determining when to emit elements, when to terminate with an error, or when to signal the end of the stream. ## From Iterables ### fromIterable You can create a pure stream from an `Iterable` of values using the `Stream.fromIterable` constructor. It's a straightforward way to convert a collection of values into a stream. ```ts twoslash import { Stream, Effect } from "effect" const numbers = [1, 2, 3] const stream = Stream.fromIterable(numbers) Effect.runPromise(Stream.runCollect(stream)).then(console.log) // { _id: 'Chunk', values: [ 1, 2, 3 ] } ``` ### fromIterableEffect When you have an effect that produces a value of type `Iterable`, you can employ the `Stream.fromIterableEffect` constructor to generate a stream from that effect. For instance, let's say you have a database operation that retrieves a list of users. Since this operation involves effects, you can utilize `Stream.fromIterableEffect` to convert the result into a `Stream`: ```ts twoslash import { Stream, Effect, Context } from "effect" class Database extends Context.Tag("Database")< Database, { readonly getUsers: Effect.Effect> } >() {} const getUsers = Database.pipe(Effect.andThen((_) => _.getUsers)) const stream = Stream.fromIterableEffect(getUsers) Effect.runPromise( Stream.runCollect( stream.pipe( Stream.provideService(Database, { getUsers: Effect.succeed(["user1", "user2"]) }) ) ) ).then(console.log) // { _id: 'Chunk', values: [ 'user1', 'user2' ] } ``` This enables you to work seamlessly with effects and convert their results into streams for further processing. ### fromAsyncIterable Async iterables are another type of data source that can be converted into a stream. With the `Stream.fromAsyncIterable` constructor, you can work with asynchronous data sources and handle potential errors gracefully. ```ts twoslash import { Stream, Effect } from "effect" const myAsyncIterable = async function* () { yield 1 yield 2 } const stream = Stream.fromAsyncIterable( myAsyncIterable(), (e) => new Error(String(e)) // Error Handling ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) // { _id: 'Chunk', values: [ 1, 2 ] } ``` In this code, we define an async iterable and then create a stream named `stream` from it. Additionally, we provide an error handler function to manage any potential errors that may occur during the conversion. ## From Repetition ### Repeating a Single Value You can create a stream that endlessly repeats a specific value using the `Stream.repeatValue` constructor: ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.repeatValue(0) Effect.runPromise(Stream.runCollect(stream.pipe(Stream.take(5)))).then( console.log ) // { _id: 'Chunk', values: [ 0, 0, 0, 0, 0 ] } ``` ### Repeating a Stream's Content `Stream.repeat` allows you to create a stream that repeats a specified stream's content according to a schedule. This can be useful for generating recurring events or values. ```ts twoslash import { Stream, Effect, Schedule } from "effect" // Creating a stream that repeats a value indefinitely const stream = Stream.repeat(Stream.succeed(1), Schedule.forever) Effect.runPromise(Stream.runCollect(stream.pipe(Stream.take(5)))).then( console.log ) // { _id: 'Chunk', values: [ 1, 1, 1, 1, 1 ] } ``` ### Repeating an Effect's Result Imagine you have an effectful API call, and you want to use the result of that call to create a stream. You can achieve this by creating a stream from the effect and repeating it indefinitely. Here's an example of generating a stream of random numbers: ```ts twoslash import { Stream, Effect, Random } from "effect" const stream = Stream.repeatEffect(Random.nextInt) Effect.runPromise(Stream.runCollect(stream.pipe(Stream.take(5)))).then( console.log ) /* Example Output: { _id: 'Chunk', values: [ 1666935266, 604851965, 2194299958, 3393707011, 4090317618 ] } */ ``` ### Repeating an Effect with Termination You can repeatedly evaluate a given effect and terminate the stream based on specific conditions. In this example, we're draining an `Iterator` to create a stream from it: ```ts twoslash import { Stream, Effect, Option } from "effect" const drainIterator = (it: Iterator): Stream.Stream => Stream.repeatEffectOption( Effect.sync(() => it.next()).pipe( Effect.andThen((res) => { if (res.done) { return Effect.fail(Option.none()) } return Effect.succeed(res.value) }) ) ) ``` ### Generating Ticks You can create a stream that emits `void` values at specified intervals using the `Stream.tick` constructor. This is useful for creating periodic events. ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.tick("100 millis") Effect.runPromise(Stream.runCollect(stream.pipe(Stream.take(5)))).then( console.log ) /* Output: { _id: 'Chunk', values: [ undefined, undefined, undefined, undefined, undefined ] } */ ``` ## From Unfolding/Pagination In functional programming, the concept of `unfold` can be thought of as the counterpart to `fold`. With `fold`, we process a data structure and produce a return value. For example, we can take an `Array` and calculate the sum of its elements. On the other hand, `unfold` represents an operation where we start with an initial value and generate a recursive data structure, adding one element at a time using a specified state function. For example, we can create a sequence of natural numbers starting from `1` and using the `increment` function as the state function. ### Unfold #### unfold The Stream module includes an `unfold` function defined as follows: ```ts showLineNumbers=false declare const unfold: ( initialState: S, step: (s: S) => Option.Option ) => Stream ``` Here's how it works: - **initialState**. This is the initial state value. - **step**. The state function `step` takes the current state `s` as input. If the result of this function is `None`, the stream ends. If it's `Some<[A, S]>`, the next element in the stream is `A`, and the state `S` is updated for the next step process. For example, let's create a stream of natural numbers using `Stream.unfold`: ```ts twoslash import { Stream, Effect, Option } from "effect" const stream = Stream.unfold(1, (n) => Option.some([n, n + 1])) Effect.runPromise(Stream.runCollect(stream.pipe(Stream.take(5)))).then( console.log ) // { _id: 'Chunk', values: [ 1, 2, 3, 4, 5 ] } ``` #### unfoldEffect Sometimes, we may need to perform effectful state transformations during the unfolding operation. This is where `Stream.unfoldEffect` comes in handy. It allows us to work with effects while generating streams. Here's an example of creating an infinite stream of random `1` and `-1` values using `Stream.unfoldEffect`: ```ts twoslash import { Stream, Effect, Option, Random } from "effect" const stream = Stream.unfoldEffect(1, (n) => Random.nextBoolean.pipe( Effect.map((b) => (b ? Option.some([n, -n]) : Option.some([n, n]))) ) ) Effect.runPromise(Stream.runCollect(stream.pipe(Stream.take(5)))).then( console.log ) // Example Output: { _id: 'Chunk', values: [ 1, 1, 1, 1, -1 ] } ``` #### Additional Variants There are also similar operations like `Stream.unfoldChunk` and `Stream.unfoldChunkEffect` tailored for working with `Chunk` data types. ### Pagination #### paginate `Stream.paginate` is similar to `Stream.unfold` but allows emitting values one step further. For example, the following stream emits `0, 1, 2, 3` elements: ```ts twoslash import { Stream, Effect, Option } from "effect" const stream = Stream.paginate(0, (n) => [ n, n < 3 ? Option.some(n + 1) : Option.none() ]) Effect.runPromise(Stream.runCollect(stream)).then(console.log) // { _id: 'Chunk', values: [ 0, 1, 2, 3 ] } ``` Here's how it works: - We start with an initial value of `0`. - The provided function takes the current value `n` and returns a tuple. The first element of the tuple is the value to emit (`n`), and the second element determines whether to continue (`Option.some(n + 1)`) or stop (`Option.none()`). #### Additional Variants There are also similar operations like `Stream.paginateChunk` and `Stream.paginateChunkEffect` tailored for working with `Chunk` data types. ### Unfolding vs. Pagination You might wonder about the difference between the `unfold` and `paginate` combinators and when to use one over the other. Let's explore this by diving into an example. Imagine we have a paginated API that provides a substantial amount of data in a paginated manner. When we make a request to this API, it returns a `ResultPage` object containing the results for the current page and a flag indicating whether it's the last page or if there's more data to retrieve on the next page. Here's a simplified representation of our API: ```ts twoslash import { Chunk, Effect } from "effect" type RawData = string class PageResult { constructor( readonly results: Chunk.Chunk, readonly isLast: boolean ) {} } const pageSize = 2 const listPaginated = ( pageNumber: number ): Effect.Effect => { return Effect.succeed( new PageResult( Chunk.map( Chunk.range(1, pageSize), (index) => `Result ${pageNumber}-${index}` ), pageNumber === 2 // Return 3 pages ) ) } ``` Our goal is to convert this paginated API into a stream of `RowData` events. For our initial attempt, we might think that using the `Stream.unfold` operation is the way to go: ```ts twoslash collapse={3-26} import { Chunk, Effect, Stream, Option } from "effect" type RawData = string class PageResult { constructor( readonly results: Chunk.Chunk, readonly isLast: boolean ) {} } const pageSize = 2 const listPaginated = ( pageNumber: number ): Effect.Effect => { return Effect.succeed( new PageResult( Chunk.map( Chunk.range(1, pageSize), (index) => `Result ${pageNumber}-${index}` ), pageNumber === 2 // Return 3 pages ) ) } const firstAttempt = Stream.unfoldChunkEffect(0, (pageNumber) => listPaginated(pageNumber).pipe( Effect.map((page) => { if (page.isLast) { return Option.none() } return Option.some([page.results, pageNumber + 1] as const) }) ) ) Effect.runPromise(Stream.runCollect(firstAttempt)).then(console.log) /* Output: { _id: "Chunk", values: [ "Result 0-1", "Result 0-2", "Result 1-1", "Result 1-2" ] } */ ``` However, this approach has a drawback, it doesn't include the results from the last page. To work around this, we perform an extra API call to include those missing results: ```ts twoslash collapse={3-26} import { Chunk, Effect, Stream, Option } from "effect" type RawData = string class PageResult { constructor( readonly results: Chunk.Chunk, readonly isLast: boolean ) {} } const pageSize = 2 const listPaginated = ( pageNumber: number ): Effect.Effect => { return Effect.succeed( new PageResult( Chunk.map( Chunk.range(1, pageSize), (index) => `Result ${pageNumber}-${index}` ), pageNumber === 2 // Return 3 pages ) ) } const secondAttempt = Stream.unfoldChunkEffect( Option.some(0), (pageNumber) => Option.match(pageNumber, { // We already hit the last page onNone: () => Effect.succeed(Option.none()), // We did not hit the last page yet onSome: (pageNumber) => listPaginated(pageNumber).pipe( Effect.map((page) => Option.some([ page.results, page.isLast ? Option.none() : Option.some(pageNumber + 1) ]) ) ) }) ) Effect.runPromise(Stream.runCollect(secondAttempt)).then(console.log) /* Output: { _id: 'Chunk', values: [ 'Result 0-1', 'Result 0-2', 'Result 1-1', 'Result 1-2', 'Result 2-1', 'Result 2-2' ] } */ ``` While this approach works, it's clear that `Stream.unfold` isn't the most friendly option for retrieving data from paginated APIs. It requires additional workarounds to include the results from the last page. This is where `Stream.paginate` comes to the rescue. It provides a more ergonomic way to convert a paginated API into an Effect stream. Let's rewrite our solution using `Stream.paginate`: ```ts twoslash collapse={3-26} import { Chunk, Effect, Stream, Option } from "effect" type RawData = string class PageResult { constructor( readonly results: Chunk.Chunk, readonly isLast: boolean ) {} } const pageSize = 2 const listPaginated = ( pageNumber: number ): Effect.Effect => { return Effect.succeed( new PageResult( Chunk.map( Chunk.range(1, pageSize), (index) => `Result ${pageNumber}-${index}` ), pageNumber === 2 // Return 3 pages ) ) } const finalAttempt = Stream.paginateChunkEffect(0, (pageNumber) => listPaginated(pageNumber).pipe( Effect.andThen((page) => { return [ page.results, page.isLast ? Option.none() : Option.some(pageNumber + 1) ] }) ) ) Effect.runPromise(Stream.runCollect(finalAttempt)).then(console.log) /* Output: { _id: 'Chunk', values: [ 'Result 0-1', 'Result 0-2', 'Result 1-1', 'Result 1-2', 'Result 2-1', 'Result 2-2' ] } */ ``` ## From Queue and PubSub In Effect, there are two essential asynchronous messaging data types: [Queue](/docs/concurrency/queue/) and [PubSub](/docs/concurrency/pubsub/). You can easily transform these data types into `Stream`s by utilizing `Stream.fromQueue` and `Stream.fromPubSub`, respectively. ## From Schedule We can create a stream from a `Schedule` that does not require any further input. The stream will emit an element for each value output from the schedule, continuing for as long as the schedule continues: ```ts twoslash import { Effect, Stream, Schedule } from "effect" // Emits values every 1 second for a total of 10 emissions const schedule = Schedule.spaced("1 second").pipe( Schedule.compose(Schedule.recurs(10)) ) const stream = Stream.fromSchedule(schedule) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ] } */ ``` # [Error Handling in Streams](https://effect.website/docs/stream/error-handling/) ## Overview ## Recovering from Failure When working with streams that may encounter errors, it's crucial to know how to handle these errors gracefully. The `Stream.orElse` function is a powerful tool for recovering from failures and switching to an alternative stream in case of an error. **Example** ```ts twoslash import { Stream, Effect } from "effect" const s1 = Stream.make(1, 2, 3).pipe( Stream.concat(Stream.fail("Oh! Error!")), Stream.concat(Stream.make(4, 5)) ) const s2 = Stream.make("a", "b", "c") const stream = Stream.orElse(s1, () => s2) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: "Chunk", values: [ 1, 2, 3, "a", "b", "c" ] } */ ``` In this example, `s1` encounters an error, but instead of terminating the stream, we gracefully switch to `s2` using `Stream.orElse`. This ensures that we can continue processing data even if one stream fails. There's also a variant called `Stream.orElseEither` that uses the [Either](/docs/data-types/either/) data type to distinguish elements from the two streams based on success or failure: ```ts twoslash import { Stream, Effect } from "effect" const s1 = Stream.make(1, 2, 3).pipe( Stream.concat(Stream.fail("Oh! Error!")), Stream.concat(Stream.make(4, 5)) ) const s2 = Stream.make("a", "b", "c") const stream = Stream.orElseEither(s1, () => s2) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: "Chunk", values: [ { _id: "Either", _tag: "Left", left: 1 }, { _id: "Either", _tag: "Left", left: 2 }, { _id: "Either", _tag: "Left", left: 3 }, { _id: "Either", _tag: "Right", right: "a" }, { _id: "Either", _tag: "Right", right: "b" }, { _id: "Either", _tag: "Right", right: "c" } ] } */ ``` The `Stream.catchAll` function provides advanced error handling capabilities compared to `Stream.orElse`. With `Stream.catchAll`, you can make decisions based on both the type and value of the encountered failure. ```ts twoslash import { Stream, Effect } from "effect" const s1 = Stream.make(1, 2, 3).pipe( Stream.concat(Stream.fail("Uh Oh!" as const)), Stream.concat(Stream.make(4, 5)), Stream.concat(Stream.fail("Ouch" as const)) ) const s2 = Stream.make("a", "b", "c") const s3 = Stream.make(true, false, false) const stream = Stream.catchAll( s1, (error): Stream.Stream => { switch (error) { case "Uh Oh!": return s2 case "Ouch": return s3 } } ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: "Chunk", values: [ 1, 2, 3, "a", "b", "c" ] } */ ``` In this example, we have a stream, `s1`, which may encounter two different types of errors. Instead of a straightforward switch to an alternative stream, as done with `Stream.orElse`, we employ `Stream.catchAll` to precisely determine how to handle each type of error. This level of control over error recovery enables you to choose different streams or actions based on the specific error conditions. ## Recovering from Defects When working with streams, it's essential to be prepared for various failure scenarios, including defects that might occur during stream processing. To address this, the `Stream.catchAllCause` function provides a robust solution. It enables you to gracefully handle and recover from any type of failure that may arise. **Example** ```ts twoslash import { Stream, Effect } from "effect" const s1 = Stream.make(1, 2, 3).pipe( Stream.concat(Stream.dieMessage("Boom!")), Stream.concat(Stream.make(4, 5)) ) const s2 = Stream.make("a", "b", "c") const stream = Stream.catchAllCause(s1, () => s2) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: "Chunk", values: [ 1, 2, 3, "a", "b", "c" ] } */ ``` In this example, `s1` may encounter a defect, but instead of crashing the application, we use `Stream.catchAllCause` to gracefully switch to an alternative stream, `s2`. This ensures that your application remains robust and continues processing data even in the face of unexpected issues. ## Recovery from Some Errors In stream processing, there may be situations where you need to recover from specific types of failures. The `Stream.catchSome` and `Stream.catchSomeCause` functions come to the rescue, allowing you to handle and mitigate errors selectively. If you want to recover from a particular error, you can use `Stream.catchSome`: ```ts twoslash import { Stream, Effect, Option } from "effect" const s1 = Stream.make(1, 2, 3).pipe( Stream.concat(Stream.fail("Oh! Error!")), Stream.concat(Stream.make(4, 5)) ) const s2 = Stream.make("a", "b", "c") const stream = Stream.catchSome(s1, (error) => { if (error === "Oh! Error!") { return Option.some(s2) } return Option.none() }) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: "Chunk", values: [ 1, 2, 3, "a", "b", "c" ] } */ ``` To recover from a specific cause, you can use the `Stream.catchSomeCause` function: ```ts twoslash import { Stream, Effect, Option, Cause } from "effect" const s1 = Stream.make(1, 2, 3).pipe( Stream.concat(Stream.dieMessage("Oh! Error!")), Stream.concat(Stream.make(4, 5)) ) const s2 = Stream.make("a", "b", "c") const stream = Stream.catchSomeCause(s1, (cause) => { if (Cause.isDie(cause)) { return Option.some(s2) } return Option.none() }) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: "Chunk", values: [ 1, 2, 3, "a", "b", "c" ] } */ ``` ## Recovering to Effect In stream processing, it's crucial to handle errors gracefully and perform cleanup tasks when needed. The `Stream.onError` function allows us to do just that. If our stream encounters an error, we can specify a cleanup task to be executed. ```ts twoslash import { Stream, Console, Effect } from "effect" const stream = Stream.make(1, 2, 3).pipe( Stream.concat(Stream.dieMessage("Oh! Boom!")), Stream.concat(Stream.make(4, 5)), Stream.onError(() => Console.log( "Stream application closed! We are doing some cleanup jobs." ).pipe(Effect.orDie) ) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: Stream application closed! We are doing some cleanup jobs. error: RuntimeException: Oh! Boom! */ ``` ## Retry a Failing Stream Sometimes, streams may encounter failures that are temporary or recoverable. In such cases, the `Stream.retry` operator comes in handy. It allows you to specify a retry schedule, and the stream will be retried according to that schedule. **Example** ```ts twoslash import { Stream, Effect, Schedule } from "effect" import * as NodeReadLine from "node:readline" const stream = Stream.make(1, 2, 3).pipe( Stream.concat( Stream.fromEffect( Effect.gen(function* () { const s = yield* readLine("Enter a number: ") const n = parseInt(s) if (Number.isNaN(n)) { return yield* Effect.fail("NaN") } return n }) ).pipe(Stream.retry(Schedule.exponential("1 second"))) ) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: Enter a number: a Enter a number: b Enter a number: c Enter a number: 4 { _id: "Chunk", values: [ 1, 2, 3, 4 ] } */ const readLine = (message: string): Effect.Effect => Effect.promise( () => new Promise((resolve) => { const rl = NodeReadLine.createInterface({ input: process.stdin, output: process.stdout }) rl.question(message, (answer) => { rl.close() resolve(answer) }) }) ) ``` In this example, the stream asks the user to input a number, but if an invalid value is entered (e.g., "a," "b," "c"), it fails with "NaN." However, we use `Stream.retry` with an exponential backoff schedule, which means it will retry after a delay of increasing duration. This allows us to handle temporary errors and eventually collect valid input. ## Refining Errors When working with streams, there might be situations where you want to selectively keep certain errors and terminate the stream with the remaining errors. You can achieve this using the `Stream.refineOrDie` function. **Example** ```ts twoslash import { Stream, Option } from "effect" const stream = Stream.fail(new Error()) const res = Stream.refineOrDie(stream, (error) => { if (error instanceof SyntaxError) { return Option.some(error) } return Option.none() }) ``` In this example, `stream` initially fails with a generic `Error`. However, we use `Stream.refineOrDie` to filter and keep only errors of type `SyntaxError`. Any other errors will be terminated, while `SyntaxErrors` will be retained in `refinedStream`. ## Timing Out When working with streams, there are scenarios where you may want to handle timeouts, such as terminating a stream if it doesn't produce a value within a certain duration. In this section, we'll explore how to manage timeouts using various operators. ### timeout The `Stream.timeout` operator allows you to set a timeout on a stream. If the stream does not produce a value within the specified duration, it terminates. ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.fromEffect(Effect.never).pipe( Stream.timeout("2 seconds") ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* { _id: "Chunk", values: [] } */ ``` ### timeoutFail The `Stream.timeoutFail` operator combines a timeout with a custom failure message. If the stream times out, it fails with the specified error message. ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.fromEffect(Effect.never).pipe( Stream.timeoutFail(() => "timeout", "2 seconds") ) Effect.runPromiseExit(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'timeout' } } */ ``` ### timeoutFailCause Similar to `Stream.timeoutFail`, `Stream.timeoutFailCause` combines a timeout with a custom failure cause. If the stream times out, it fails with the specified cause. ```ts twoslash import { Stream, Effect, Cause } from "effect" const stream = Stream.fromEffect(Effect.never).pipe( Stream.timeoutFailCause(() => Cause.die("timeout"), "2 seconds") ) Effect.runPromiseExit(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Die', defect: 'timeout' } } */ ``` ### timeoutTo The `Stream.timeoutTo` operator allows you to switch to another stream if the first stream does not produce a value within the specified duration. ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.fromEffect(Effect.never).pipe( Stream.timeoutTo("2 seconds", Stream.make(1, 2, 3)) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* { _id: "Chunk", values: [ 1, 2, 3 ] } */ ``` # [Introduction to Streams](https://effect.website/docs/stream/introduction/) ## Overview In this guide, we'll explore the concept of a `Stream`. A `Stream` is a program description that, when executed, can emit **zero or more values** of type `A`, handle errors of type `E`, and operates within a context of type `R`. ## Use Cases Streams are particularly handy whenever you're dealing with sequences of values over time. They can serve as replacements for observables, node streams, and AsyncIterables. ## What is a Stream? Think of a `Stream` as an extension of an `Effect`. While an `Effect` represents a program that requires a context of type `R`, may encounter an error of type `E`, and always produces a single result of type `A`, a `Stream` takes this further by allowing the emission of zero or more values of type `A`. To clarify, let's examine some examples using `Effect`: ```ts twoslash import { Effect, Chunk, Option } from "effect" // An Effect that fails with a string error const failedEffect = Effect.fail("fail!") // An Effect that produces a single number const oneNumberValue = Effect.succeed(3) // An Effect that produces a chunk of numbers const oneListValue = Effect.succeed(Chunk.make(1, 2, 3)) // An Effect that produces an optional number const oneOption = Effect.succeed(Option.some(1)) ``` In each case, the `Effect` always ends with **exactly one value**. There is no variability; you always get one result. ## Understanding Streams Now, let's shift our focus to `Stream`. A `Stream` represents a program description that shares similarities with `Effect`, it requires a context of type `R`, may signal errors of type `E`, and yields values of type `A`. However, the key distinction is that it can yield **zero or more values**. Here are the possible scenarios for a `Stream`: - **An Empty Stream**: It can end up empty, representing a stream with no values. - **A Single-Element Stream**: It can represent a stream with just one value. - **A Finite Stream of Elements**: It can represent a stream with a finite number of values. - **An Infinite Stream of Elements**: It can represent a stream that continues indefinitely, essentially an infinite stream. Let's see these scenarios in action: ```ts twoslash import { Stream } from "effect" // An empty Stream const emptyStream = Stream.empty // A Stream with a single number const oneNumberValueStream = Stream.succeed(3) // A Stream with a range of numbers from 1 to 10 const finiteNumberStream = Stream.range(1, 10) // An infinite Stream of numbers starting from 1 and incrementing const infiniteNumberStream = Stream.iterate(1, (n) => n + 1) ``` In summary, a `Stream` is a versatile tool for representing programs that may yield multiple values, making it suitable for a wide range of tasks, from processing finite lists to handling infinite sequences. # [Operations](https://effect.website/docs/stream/operations/) ## Overview import { Aside } from "@astrojs/starlight/components" In this guide, we'll explore some essential operations you can perform on streams. These operations allow you to manipulate and interact with stream elements in various ways. ## Tapping The `Stream.tap` operation allows you to run an effect on each element emitted by the stream, observing or performing side effects without altering the elements or return type. This can be useful for logging, monitoring, or triggering additional actions with each emission. **Example** (Logging with `Stream.tap`) For example, `Stream.tap` can be used to log each element before and after a mapping operation: ```ts twoslash import { Stream, Console, Effect } from "effect" const stream = Stream.make(1, 2, 3).pipe( Stream.tap((n) => Console.log(`before mapping: ${n}`)), Stream.map((n) => n * 2), Stream.tap((n) => Console.log(`after mapping: ${n}`)) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: before mapping: 1 after mapping: 2 before mapping: 2 after mapping: 4 before mapping: 3 after mapping: 6 { _id: 'Chunk', values: [ 2, 4, 6 ] } */ ``` ## Taking Elements The "taking" operations in streams let you extract a specific set of elements, either by a fixed number, condition, or position within the stream. Here are a few ways to apply these operations: | API | Description | | ----------- | ----------------------------------------------------- | | `take` | Extracts a fixed number of elements. | | `takeWhile` | Extracts elements while a certain condition is met. | | `takeUntil` | Extracts elements until a certain condition is met. | | `takeRight` | Extracts a specified number of elements from the end. | **Example** (Extracting Elements in Different Ways) ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.iterate(0, (n) => n + 1) // Using `take` to extract a fixed number of elements: const s1 = Stream.take(stream, 5) Effect.runPromise(Stream.runCollect(s1)).then(console.log) /* Output: { _id: 'Chunk', values: [ 0, 1, 2, 3, 4 ] } */ // Using `takeWhile` to extract elements while a condition is met: const s2 = Stream.takeWhile(stream, (n) => n < 5) Effect.runPromise(Stream.runCollect(s2)).then(console.log) /* Output: { _id: 'Chunk', values: [ 0, 1, 2, 3, 4 ] } */ // Using `takeUntil` to extract elements until a condition is met: const s3 = Stream.takeUntil(stream, (n) => n === 5) Effect.runPromise(Stream.runCollect(s3)).then(console.log) /* Output: { _id: 'Chunk', values: [ 0, 1, 2, 3, 4, 5 ] } */ // Using `takeRight` to take elements from the end of the stream: const s4 = Stream.takeRight(s3, 3) Effect.runPromise(Stream.runCollect(s4)).then(console.log) /* Output: { _id: 'Chunk', values: [ 3, 4, 5 ] } */ ``` ## Streams as an Alternative to Async Iterables When working with asynchronous data sources, such as async iterables, you often need to consume data in a loop until a certain condition is met. Streams provide a similar approach and offer additional flexibility. With async iterables, data is processed in a loop until a break or return statement is encountered. To replicate this behavior with Streams, consider these options: | API | Description | | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `takeUntil` | Takes elements from a stream until a specified condition is met, similar to breaking out of a loop. | | `toPull` | Returns an effect that continuously pulls data chunks from the stream. This effect can fail with `None` when the stream is finished or with `Some` error if it fails. | **Example** (Using `Stream.toPull`) ```ts twoslash import { Stream, Effect } from "effect" // Simulate a chunked stream const stream = Stream.fromIterable([1, 2, 3, 4, 5]).pipe( Stream.rechunk(2) ) const program = Effect.gen(function* () { // Create an effect to get data chunks from the stream const getChunk = yield* Stream.toPull(stream) // Continuously fetch and process chunks while (true) { const chunk = yield* getChunk console.log(chunk) } }) Effect.runPromise(Effect.scoped(program)).then(console.log, console.error) /* Output: { _id: 'Chunk', values: [ 1, 2 ] } { _id: 'Chunk', values: [ 3, 4 ] } { _id: 'Chunk', values: [ 5 ] } (FiberFailure) Error: { "_id": "Option", "_tag": "None" } */ ``` ## Mapping ### Basic Mapping The `Stream.map` operation applies a specified function to each element in a stream, creating a new stream with the transformed values. **Example** (Incrementing Each Element by 1) ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.make(1, 2, 3).pipe( Stream.map((n) => n + 1) // Increment each element by 1 ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ 2, 3, 4 ] } */ ``` ### Mapping to a Constant Value The `Stream.as` method allows you to replace each success value in a stream with a specified constant value. This can be useful when you want all elements in the stream to emit a uniform value, regardless of the original data. **Example** (Mapping to `null`) ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.range(1, 5).pipe(Stream.as(null)) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ null, null, null, null, null ] } */ ``` ### Effectful Mapping For transformations involving effects, use `Stream.mapEffect`. This function applies an effectful operation to each element in the stream, producing a new stream with effectful results. **Example** (Random Number Generation) ```ts twoslash import { Stream, Random, Effect } from "effect" const stream = Stream.make(10, 20, 30).pipe( // Generate a random number between 0 and each element Stream.mapEffect((n) => Random.nextIntBetween(0, n)) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Example Output: { _id: 'Chunk', values: [ 5, 9, 22 ] } */ ``` To handle multiple effectful transformations concurrently, you can use the [concurrency](/docs/concurrency/basic-concurrency/#concurrency-options) option. This option allows a specified number of effects to run concurrently, with results emitted downstream in their original order. **Example** (Fetching URLs Concurrently) ```ts twoslash import { Stream, Effect } from "effect" const fetchUrl = (url: string) => Effect.gen(function* () { console.log(`Fetching ${url}`) yield* Effect.sleep("100 millis") console.log(`Fetching ${url} done`) return [`Resource 0-${url}`, `Resource 1-${url}`, `Resource 2-${url}`] }) const stream = Stream.make("url1", "url2", "url3").pipe( // Fetch each URL concurrently with a limit of 2 Stream.mapEffect(fetchUrl, { concurrency: 2 }) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: Fetching url1 Fetching url2 Fetching url1 done Fetching url3 Fetching url2 done Fetching url3 done { _id: 'Chunk', values: [ [ 'Resource 0-url1', 'Resource 1-url1', 'Resource 2-url1' ], [ 'Resource 0-url2', 'Resource 1-url2', 'Resource 2-url2' ], [ 'Resource 0-url3', 'Resource 1-url3', 'Resource 2-url3' ] ] } */ ``` ### Stateful Mapping `Stream.mapAccum` is similar to `Stream.map`, but it applies a transformation with state tracking, allowing you to map and accumulate values within a single operation. This is useful for tasks like calculating a running total in a stream. **Example** (Calculating a Running Total) ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.range(1, 5).pipe( // ┌─── next state // │ ┌─── emitted value // ▼ ▼ Stream.mapAccum(0, (state, n) => [state + n, state + n]) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ 1, 3, 6, 10, 15 ] } */ ``` ### Mapping and Flattening The `Stream.mapConcat` operation is similar to `Stream.map`, but it goes further by mapping each element to zero or more elements (as an `Iterable`) and then flattening the entire stream. This is particularly useful for transforming each element into multiple values. **Example** (Splitting and Flattening a Stream) ```ts twoslash import { Stream, Effect } from "effect" const numbers = Stream.make("1-2-3", "4-5", "6").pipe( Stream.mapConcat((s) => s.split("-")) ) Effect.runPromise(Stream.runCollect(numbers)).then(console.log) /* Output: { _id: 'Chunk', values: [ '1', '2', '3', '4', '5', '6' ] } */ ``` ## Filtering The `Stream.filter` operation allows you to pass through only elements that meet a specific condition. It's a way to retain elements in a stream that satisfy a particular criteria while discarding the rest. **Example** (Filtering Even Numbers) ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.range(1, 11).pipe(Stream.filter((n) => n % 2 === 0)) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ 2, 4, 6, 8, 10 ] } */ ``` ## Scanning Stream scanning allows you to apply a function cumulatively to each element in the stream, emitting every intermediate result. Unlike `reduce`, which only provides a final result, `scan` offers a step-by-step view of the accumulation process. **Example** (Cumulative Addition) ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.range(1, 5).pipe(Stream.scan(0, (a, b) => a + b)) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ 0, 1, 3, 6, 10, 15 ] } */ ``` If you need only the final accumulated value, you can use [Stream.runFold](/docs/stream/consuming-streams/#using-a-fold-operation): **Example** (Final Accumulated Result) ```ts twoslash import { Stream, Effect } from "effect" const fold = Stream.range(1, 5).pipe(Stream.runFold(0, (a, b) => a + b)) Effect.runPromise(fold).then(console.log) // Output: 15 ``` ## Draining Stream draining lets you execute effectful operations within a stream while discarding the resulting values. This can be useful when you need to run actions or perform side effects but don't require the emitted values. The `Stream.drain` function achieves this by ignoring all elements in the stream and producing an empty output stream. **Example** (Executing Effectful Operations without Collecting Values) ```ts twoslash import { Stream, Effect, Random } from "effect" const stream = Stream.repeatEffect( Effect.gen(function* () { const nextInt = yield* Random.nextInt const number = Math.abs(nextInt % 10) console.log(`random number: ${number}`) return number }) ).pipe(Stream.take(3)) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Example Output: random number: 7 random number: 5 random number: 0 { _id: 'Chunk', values: [ 7, 5, 0 ] } */ const drained = Stream.drain(stream) Effect.runPromise(Stream.runCollect(drained)).then(console.log) /* Example Output: random number: 0 random number: 1 random number: 7 { _id: 'Chunk', values: [] } */ ``` ## Detecting Changes in a Stream The `Stream.changes` operation detects and emits elements that differ from their preceding elements within a stream. This can be useful for tracking changes or deduplicating consecutive values. **Example** (Emitting Distinct Consecutive Elements) ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.make(1, 1, 1, 2, 2, 3, 4).pipe(Stream.changes) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ 1, 2, 3, 4 ] } */ ``` ## Zipping Zipping combines elements from two streams into a new stream, pairing elements from each input stream. This can be achieved with `Stream.zip` or `Stream.zipWith`, allowing for custom pairing logic. **Example** (Basic Zipping) In this example, elements from the two streams are paired sequentially. The resulting stream ends when one of the streams is exhausted. ```ts twoslash import { Stream, Effect } from "effect" // Zip two streams together const stream = Stream.zip( Stream.make(1, 2, 3, 4, 5, 6), Stream.make("a", "b", "c") ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ [ 1, 'a' ], [ 2, 'b' ], [ 3, 'c' ] ] } */ ``` **Example** (Custom Zipping Logic) Here, `Stream.zipWith` applies custom logic to each pair, combining elements in a user-defined way. ```ts twoslash import { Stream, Effect } from "effect" // Zip two streams with custom pairing logic const stream = Stream.zipWith( Stream.make(1, 2, 3, 4, 5, 6), Stream.make("a", "b", "c"), (n, s) => [n + 10, s + "!"] ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ [ 11, 'a!' ], [ 12, 'b!' ], [ 13, 'c!' ] ] } */ ``` ### Handling Stream Endings If one input stream ends before the other, you might want to zip with default values to avoid missing pairs. The `Stream.zipAll` and `Stream.zipAllWith` operators provide this functionality, allowing you to specify defaults for either stream. **Example** (Zipping with Default Values) In this example, when the second stream completes, the first stream continues with "x" as a default value for the second stream. ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.zipAll(Stream.make(1, 2, 3, 4, 5, 6), { other: Stream.make("a", "b", "c"), defaultSelf: -1, defaultOther: "x" }) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ [ 1, 'a' ], [ 2, 'b' ], [ 3, 'c' ], [ 4, 'x' ], [ 5, 'x' ], [ 6, 'x' ] ] } */ ``` **Example** (Custom Logic with zipAllWith) With `Stream.zipAllWith`, custom logic determines how to combine elements when either stream runs out, offering flexibility to handle these cases. ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.zipAllWith(Stream.make(1, 2, 3, 4, 5, 6), { other: Stream.make("a", "b", "c"), onSelf: (n) => [n, "x"], onOther: (s) => [-1, s], onBoth: (n, s) => [n + 10, s + "!"] }) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ [ 11, 'a!' ], [ 12, 'b!' ], [ 13, 'c!' ], [ 4, 'x' ], [ 5, 'x' ], [ 6, 'x' ] ] } */ ``` ### Zipping Streams at Different Rates When combining streams that emit elements at different speeds, you may not want to wait for the slower stream to emit. Using `Stream.zipLatest` or `Stream.zipLatestWith`, you can zip elements as soon as either stream produces a new value. These functions use the most recent element from the slower stream whenever a new value arrives from the faster stream. **Example** (Combining Streams with Different Emission Rates) ```ts twoslash import { Stream, Schedule, Effect } from "effect" const s1 = Stream.make(1, 2, 3).pipe( Stream.schedule(Schedule.spaced("1 second")) ) const s2 = Stream.make("a", "b", "c", "d").pipe( Stream.schedule(Schedule.spaced("500 millis")) ) const stream = Stream.zipLatest(s1, s2) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ [ 1, 'a' ], // s1 emits 1 and pairs with the latest value from s2 [ 1, 'b' ], // s2 emits 'b', pairs with the latest value from s1 [ 2, 'b' ], // s1 emits 2, pairs with the latest value from s2 [ 2, 'c' ], // s2 emits 'c', pairs with the latest value from s1 [ 2, 'd' ], // s2 emits 'd', pairs with the latest value from s1 [ 3, 'd' ] // s1 emits 3, pairs with the latest value from s2 ] } */ ``` ### Pairing with Previous and Next Elements | API | Description | | ------------------------ | --------------------------------------------------------- | | `zipWithPrevious` | Pairs each element of a stream with its previous element. | | `zipWithNext` | Pairs each element of a stream with its next element. | | `zipWithPreviousAndNext` | Pairs each element with both its previous and next. | **Example** (Pairing Stream Elements with Next) ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.zipWithNext(Stream.make(1, 2, 3, 4)) Effect.runPromise(Stream.runCollect(stream)).then((chunks) => console.log("%o", chunks) ) /* Output: { _id: 'Chunk', values: [ [ 1, { _id: 'Option', _tag: 'Some', value: 2 }, [length]: 2 ], [ 2, { _id: 'Option', _tag: 'Some', value: 3 }, [length]: 2 ], [ 3, { _id: 'Option', _tag: 'Some', value: 4 }, [length]: 2 ], [ 4, { _id: 'Option', _tag: 'None' }, [length]: 2 ], [length]: 4 ] } */ ``` ### Indexing Stream Elements The `Stream.zipWithIndex` operator is a helpful tool for indexing each element in a stream, pairing each item with its respective position in the sequence. This is particularly useful when you want to keep track of the order of elements within a stream. **Example** (Indexing Each Element in a Stream) ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.zipWithIndex( Stream.make("Mary", "James", "Robert", "Patricia") ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ [ 'Mary', 0 ], [ 'James', 1 ], [ 'Robert', 2 ], [ 'Patricia', 3 ] ] } */ ``` ## Cartesian Product of Streams The Stream module includes a feature for computing the _Cartesian Product_ of two streams, allowing you to create combinations of elements from two different streams. This is helpful when you need to pair each element from one set with every element of another. In simple terms, imagine you have two collections and want to form all possible pairs by picking one item from each. This pairing process is the Cartesian Product. In streams, this operation generates a new stream that includes every possible pairing of elements from the two input streams. To create a Cartesian Product of two streams, the `Stream.cross` operator is available, along with similar variants. These operators combine two streams into a new stream of all possible element combinations. **Example** (Creating a Cartesian Product of Two Streams) ```ts twoslash import { Stream, Effect, Console } from "effect" const s1 = Stream.make(1, 2, 3).pipe(Stream.tap(Console.log)) const s2 = Stream.make("a", "b").pipe(Stream.tap(Console.log)) const cartesianProduct = Stream.cross(s1, s2) Effect.runPromise(Stream.runCollect(cartesianProduct)).then(console.log) /* Output: 1 a b 2 a b 3 a b { _id: 'Chunk', values: [ [ 1, 'a' ], [ 1, 'b' ], [ 2, 'a' ], [ 2, 'b' ], [ 3, 'a' ], [ 3, 'b' ] ] } */ ``` ## Partitioning Partitioning a stream involves dividing it into two distinct streams based on a specified condition. The Stream module offers two functions for this purpose: `Stream.partition` and `Stream.partitionEither`. Let's look at how these functions work and the best scenarios for their use. ### partition The `Stream.partition` function takes a predicate (a condition) as input and divides the original stream into two substreams. One substream will contain elements that meet the condition, while the other contains those that do not. Both resulting substreams are wrapped in a `Scope` type. **Example** (Partitioning a Stream into Odd and Even Numbers) ```ts twoslash import { Stream, Effect } from "effect" // ┌─── Effect<[Stream, Stream], never, Scope> // ▼ const program = Stream.range(1, 9).pipe( Stream.partition((n) => n % 2 === 0, { bufferSize: 5 }) ) Effect.runPromise( Effect.scoped( Effect.gen(function* () { const [odds, evens] = yield* program console.log(yield* Stream.runCollect(odds)) console.log(yield* Stream.runCollect(evens)) }) ) ) /* Output: { _id: 'Chunk', values: [ 1, 3, 5, 7, 9 ] } { _id: 'Chunk', values: [ 2, 4, 6, 8 ] } */ ``` ### partitionEither In some cases, you might need to partition a stream using a condition that involves an effect. For this, the `Stream.partitionEither` function is ideal. This function uses an effectful predicate to split the stream into two substreams: one for elements that produce `Either.left` values and another for elements that produce `Either.right` values. **Example** (Partitioning a Stream with an Effectful Predicate) ```ts twoslash import { Stream, Effect, Either } from "effect" // ┌─── Effect<[Stream, Stream], never, Scope> // ▼ const program = Stream.range(1, 9).pipe( Stream.partitionEither( // Simulate an effectful computation (n) => Effect.succeed(n % 2 === 0 ? Either.right(n) : Either.left(n)), { bufferSize: 5 } ) ) Effect.runPromise( Effect.scoped( Effect.gen(function* () { const [odds, evens] = yield* program console.log(yield* Stream.runCollect(odds)) console.log(yield* Stream.runCollect(evens)) }) ) ) /* Output: { _id: 'Chunk', values: [ 1, 3, 5, 7, 9 ] } { _id: 'Chunk', values: [ 2, 4, 6, 8 ] } */ ``` ## Grouping When processing streams of data, you may need to group elements based on specific criteria. The Stream module provides two functions for this purpose: `groupByKey`, `groupBy`, `grouped` and `groupedWithin`. Let's review how these functions work and when to use each one. ### groupByKey The `Stream.groupByKey` function partitions a stream based on a key function of type `(a: A) => K`, where `A` is the type of elements in the stream, and `K` represents the keys for grouping. This function is non-effectful and groups elements by simply applying the provided key function. The result of `Stream.groupByKey` is a `GroupBy` data type, representing the grouped stream. To process each group, you can use `GroupBy.evaluate`, which takes a function of type `(key: K, stream: Stream) => Stream.Stream<...>`. This function operates across all groups and merges them together in a non-deterministic order. **Example** (Grouping by Tens Place in Exam Scores) In the following example, we use `Stream.groupByKey` to group exam scores by the tens place and count the number of scores in each group: ```ts twoslash import { Stream, GroupBy, Effect, Chunk } from "effect" class Exam { constructor(readonly person: string, readonly score: number) {} } // Define a list of exam results const examResults = [ new Exam("Alex", 64), new Exam("Michael", 97), new Exam("Bill", 77), new Exam("John", 78), new Exam("Bobby", 71) ] // Group exam results by the tens place in the score const groupByKeyResult = Stream.fromIterable(examResults).pipe( Stream.groupByKey((exam) => Math.floor(exam.score / 10) * 10) ) // Count the number of exam results in each group const stream = GroupBy.evaluate(groupByKeyResult, (key, stream) => Stream.fromEffect( Stream.runCollect(stream).pipe( Effect.andThen((chunk) => [key, Chunk.size(chunk)] as const) ) ) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ [ 60, 1 ], [ 90, 1 ], [ 70, 3 ] ] } */ ``` ### groupBy For more complex grouping requirements where partitioning involves effects, you can use the `Stream.groupBy` function. This function accepts an effectful partitioning function and returns a `GroupBy` data type, representing the grouped stream. You can then process each group by using `GroupBy.evaluate`, similar to `Stream.groupByKey`. **Example** (Grouping Names by First Letter) In the following example, we group names by their first letter and count the number of names in each group. Here, the partitioning operation is set up as an effectful operation: ```ts twoslash import { Stream, GroupBy, Effect, Chunk } from "effect" // Group names by their first letter const groupByKeyResult = Stream.fromIterable([ "Mary", "James", "Robert", "Patricia", "John", "Jennifer", "Rebecca", "Peter" ]).pipe( // Simulate an effectful groupBy operation Stream.groupBy((name) => Effect.succeed([name.substring(0, 1), name])) ) // Count the number of names in each group and display results const stream = GroupBy.evaluate(groupByKeyResult, (key, stream) => Stream.fromEffect( Stream.runCollect(stream).pipe( Effect.andThen((chunk) => [key, Chunk.size(chunk)] as const) ) ) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ [ 'M', 1 ], [ 'J', 3 ], [ 'R', 2 ], [ 'P', 2 ] ] } */ ``` ### grouped The `Stream.grouped` function is ideal for dividing a stream into chunks of a specified size, making it easier to handle data in smaller, organized segments. This is particularly helpful when processing or displaying data in batches. **Example** (Dividing a Stream into Chunks of 3 Elements) ```ts twoslash import { Stream, Effect } from "effect" // Create a stream of numbers and group them into chunks of 3 const stream = Stream.range(0, 8).pipe(Stream.grouped(3)) Effect.runPromise(Stream.runCollect(stream)).then((chunks) => console.log("%o", chunks) ) /* Output: { _id: 'Chunk', values: [ { _id: 'Chunk', values: [ 0, 1, 2, [length]: 3 ] }, { _id: 'Chunk', values: [ 3, 4, 5, [length]: 3 ] }, { _id: 'Chunk', values: [ 6, 7, 8, [length]: 3 ] }, [length]: 3 ] } */ ``` ### groupedWithin The `Stream.groupedWithin` function allows for flexible grouping by creating chunks based on either a specified maximum size or a time interval, whichever condition is met first. This is especially useful for working with data where timing constraints are involved. **Example** (Grouping by Size or Time Interval) In this example, `Stream.groupedWithin(18, "1.5 seconds")` groups the stream into chunks whenever either 18 elements accumulate or 1.5 seconds elapse since the last chunk was created. ```ts twoslash import { Stream, Schedule, Effect, Chunk } from "effect" // Create a stream that repeats every second and group by size or time const stream = Stream.range(0, 9).pipe( Stream.repeat(Schedule.spaced("1 second")), Stream.groupedWithin(18, "1.5 seconds"), Stream.take(3) ) Effect.runPromise(Stream.runCollect(stream)).then((chunks) => console.log(Chunk.toArray(chunks)) ) /* Output: [ { _id: 'Chunk', values: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7 ] }, { _id: 'Chunk', values: [ 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ] }, { _id: 'Chunk', values: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7 ] } ] */ ``` ## Concatenation In stream processing, you may need to combine the contents of multiple streams. The Stream module offers several operators to achieve this, including `Stream.concat`, `Stream.concatAll`, and `Stream.flatMap`. Let's look at how each of these operators works. ### Simple Concatenation The `Stream.concat` operator is a straightforward method for joining two streams. It returns a new stream that emits elements from the first stream (left-hand) followed by elements from the second stream (right-hand). This is helpful when you want to combine two streams in a specific sequence. **Example** (Concatenating Two Streams Sequentially) ```ts twoslash import { Stream, Effect } from "effect" const stream = Stream.concat(Stream.make(1, 2, 3), Stream.make("a", "b")) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ 1, 2, 3, 'a', 'b' ] } */ ``` ### Concatenating Multiple Streams If you have multiple streams to concatenate, `Stream.concatAll` provides an efficient way to combine them without manually chaining multiple `Stream.concat` operations. This function takes a [Chunk](/docs/data-types/chunk/) of streams and returns a single stream containing the elements of each stream in sequence. **Example** (Concatenating Multiple Streams) ```ts twoslash import { Stream, Effect, Chunk } from "effect" const s1 = Stream.make(1, 2, 3) const s2 = Stream.make("a", "b") const s3 = Stream.make(true, false, false) const stream = Stream.concatAll( Chunk.make(s1, s2, s3) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ 1, 2, 3, 'a', 'b', true, false, false ] } */ ``` ### Advanced Concatenation with flatMap The `Stream.flatMap` operator allows for advanced concatenation by creating a stream where each element is generated by applying a function of type `(a: A) => Stream<...>` to each output of the source stream. This operator then concatenates all the resulting streams, effectively flattening them. **Example** (Generating Repeated Elements with `Stream.flatMap`) ```ts twoslash import { Stream, Effect } from "effect" // Create a stream where each element is repeated 4 times const stream = Stream.make(1, 2, 3).pipe( Stream.flatMap((a) => Stream.repeatValue(a).pipe(Stream.take(4))) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3 ] } */ ``` If you need to perform the `flatMap` operation concurrently, you can use the [concurrency](/docs/concurrency/basic-concurrency/#concurrency-options) option to control how many inner streams run simultaneously. Additionally, if the order of concatenation is not important, you can use the `switch` option. ## Merging Sometimes, you may want to interleave elements from two streams and create a single output stream. In such cases, `Stream.concat` isn't suitable because it waits for the first stream to complete before consuming the second. For interleaving elements as they become available, `Stream.merge` and its variants are designed for this purpose. ### merge The `Stream.merge` operation combines elements from two source streams into a single stream, interleaving elements as they are produced. Unlike `Stream.concat`, `Stream.merge` does not wait for one stream to finish before starting the other. **Example** (Interleaving Two Streams with `Stream.merge`) ```ts twoslash import { Schedule, Stream, Effect } from "effect" // Create two streams with different emission intervals const s1 = Stream.make(1, 2, 3).pipe( Stream.schedule(Schedule.spaced("100 millis")) ) const s2 = Stream.make(4, 5, 6).pipe( Stream.schedule(Schedule.spaced("200 millis")) ) // Merge s1 and s2 into a single stream that interleaves their values const merged = Stream.merge(s1, s2) Effect.runPromise(Stream.runCollect(merged)).then(console.log) /* Output: { _id: 'Chunk', values: [ 1, 4, 2, 3, 5, 6 ] } */ ``` ### Termination Strategy When merging two streams, it's important to consider the termination strategy, especially if each stream has a different lifetime. By default, `Stream.merge` waits for both streams to terminate before ending the merged stream. However, you can modify this behavior with `haltStrategy`, selecting from four termination strategies: | Termination Strategy | Description | | -------------------- | -------------------------------------------------------------------- | | `"left"` | The merged stream terminates when the left-hand stream terminates. | | `"right"` | The merged stream terminates when the right-hand stream terminates. | | `"both"` (default) | The merged stream terminates only when both streams have terminated. | | `"either"` | The merged stream terminates as soon as either stream terminates. | **Example** (Using `haltStrategy: "left"` to Control Stream Termination) ```ts twoslash import { Stream, Schedule, Effect } from "effect" const s1 = Stream.range(1, 5).pipe( Stream.schedule(Schedule.spaced("100 millis")) ) const s2 = Stream.repeatValue(0).pipe( Stream.schedule(Schedule.spaced("200 millis")) ) const merged = Stream.merge(s1, s2, { haltStrategy: "left" }) Effect.runPromise(Stream.runCollect(merged)).then(console.log) /* Output: { _id: 'Chunk', values: [ 1, 0, 2, 3, 0, 4, 5 ] } */ ``` ### mergeWith In some cases, you may want to merge two streams while transforming their elements into a unified type. `Stream.mergeWith` is designed for this purpose, allowing you to specify transformation functions for each source stream. **Example** (Merging and Transforming Two Streams) ```ts twoslash import { Schedule, Stream, Effect } from "effect" const s1 = Stream.make("1", "2", "3").pipe( Stream.schedule(Schedule.spaced("100 millis")) ) const s2 = Stream.make(4.1, 5.3, 6.2).pipe( Stream.schedule(Schedule.spaced("200 millis")) ) const merged = Stream.mergeWith(s1, s2, { // Convert string elements from `s1` to integers onSelf: (s) => parseInt(s), // Round down decimal elements from `s2` onOther: (n) => Math.floor(n) }) Effect.runPromise(Stream.runCollect(merged)).then(console.log) /* Output: { _id: 'Chunk', values: [ 1, 4, 2, 3, 5, 6 ] } */ ``` ## Interleaving ### interleave The `Stream.interleave` operator lets you pull one element at a time from each of two streams, creating a new interleaved stream. If one stream finishes first, the remaining elements from the other stream continue to be pulled until both streams are exhausted. **Example** (Basic Interleaving of Two Streams) ```ts twoslash import { Stream, Effect } from "effect" const s1 = Stream.make(1, 2, 3) const s2 = Stream.make(4, 5, 6) const stream = Stream.interleave(s1, s2) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ 1, 4, 2, 5, 3, 6 ] } */ ``` ### interleaveWith For more complex interleaving, `Stream.interleaveWith` provides additional control by using a third stream of `boolean` values to dictate the interleaving pattern. When this stream emits `true`, an element is taken from the left-hand stream; otherwise, an element is taken from the right-hand stream. **Example** (Custom Interleaving Logic Using `Stream.interleaveWith`) ```ts twoslash import { Stream, Effect } from "effect" const s1 = Stream.make(1, 3, 5, 7, 9) const s2 = Stream.make(2, 4, 6, 8, 10) // Define a boolean stream to control interleaving const booleanStream = Stream.make(true, false, false).pipe(Stream.forever) const stream = Stream.interleaveWith(s1, s2, booleanStream) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ 1, 2, 4, 3, 6, 8, 5, 10, 7, 9 ] } */ ``` ## Interspersing Interspersing adds separators or affixes in a stream, useful for formatting or structuring data in streams. ### intersperse The `Stream.intersperse` operator inserts a specified delimiter element between each pair of elements in a stream. This delimiter can be any chosen value and is added between each consecutive pair. **Example** (Inserting Delimiters Between Stream Elements) ```ts twoslash import { Stream, Effect } from "effect" // Create a stream of numbers and intersperse `0` between them const stream = Stream.make(1, 2, 3, 4, 5).pipe(Stream.intersperse(0)) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ 1, 0, 2, 0, 3, 0, 4, 0, 5 ] } */ ``` ### intersperseAffixes For more complex needs, `Stream.intersperseAffixes` provides control over different affixes at the start, between elements, and at the end of the stream. **Example** (Adding Affixes to a Stream) ```ts twoslash import { Stream, Effect } from "effect" // Create a stream and add affixes: // - `[` at the start // - `|` between elements // - `]` at the end const stream = Stream.make(1, 2, 3, 4, 5).pipe( Stream.intersperseAffixes({ start: "[", middle: "|", end: "]" }) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: { _id: 'Chunk', values: [ '[', 1, '|', 2, '|', 3, '|', 4, '|', 5, ']' ] } */ ``` ## Broadcasting Broadcasting a stream creates multiple downstream streams that each receive the same elements from the source stream. This is useful when you want to send each element to multiple consumers simultaneously. The upstream stream has a `maximumLag` parameter that sets the limit for how much it can get ahead before slowing down to match the speed of the slowest downstream stream. **Example** (Broadcasting to Multiple Downstream Streams) In the following example, we broadcast a stream of numbers to two downstream consumers. The first calculates the maximum value in the stream, while the second logs each number with a delay. The upstream stream's speed adjusts based on the slower logging stream: ```ts twoslash import { Effect, Stream, Console, Schedule, Fiber } from "effect" const numbers = Effect.scoped( Stream.range(1, 20).pipe( Stream.tap((n) => Console.log(`Emit ${n} element before broadcasting`) ), // Broadcast to 2 downstream consumers with max lag of 5 Stream.broadcast(2, 5), Stream.flatMap(([first, second]) => Effect.gen(function* () { // First downstream stream: calculates maximum const fiber1 = yield* Stream.runFold(first, 0, (acc, e) => Math.max(acc, e) ).pipe( Effect.andThen((max) => Console.log(`Maximum: ${max}`)), Effect.fork ) // Second downstream stream: logs each element with a delay const fiber2 = yield* second.pipe( Stream.schedule(Schedule.spaced("1 second")), Stream.runForEach((n) => Console.log(`Logging to the Console: ${n}`) ), Effect.fork ) // Wait for both fibers to complete yield* Fiber.join(fiber1).pipe( Effect.zip(Fiber.join(fiber2), { concurrent: true }) ) }) ), Stream.runCollect ) ) Effect.runPromise(numbers).then(console.log) /* Output: Emit 1 element before broadcasting Emit 2 element before broadcasting Emit 3 element before broadcasting Emit 4 element before broadcasting Emit 5 element before broadcasting Emit 6 element before broadcasting Emit 7 element before broadcasting Emit 8 element before broadcasting Emit 9 element before broadcasting Emit 10 element before broadcasting Emit 11 element before broadcasting Logging to the Console: 1 Logging to the Console: 2 Logging to the Console: 3 Logging to the Console: 4 Logging to the Console: 5 Emit 12 element before broadcasting Emit 13 element before broadcasting Emit 14 element before broadcasting Emit 15 element before broadcasting Emit 16 element before broadcasting Logging to the Console: 6 Logging to the Console: 7 Logging to the Console: 8 Logging to the Console: 9 Logging to the Console: 10 Emit 17 element before broadcasting Emit 18 element before broadcasting Emit 19 element before broadcasting Emit 20 element before broadcasting Logging to the Console: 11 Logging to the Console: 12 Logging to the Console: 13 Logging to the Console: 14 Logging to the Console: 15 Maximum: 20 Logging to the Console: 16 Logging to the Console: 17 Logging to the Console: 18 Logging to the Console: 19 Logging to the Console: 20 { _id: 'Chunk', values: [ undefined ] } */ ``` ## Buffering Effect streams use a pull-based model, allowing downstream consumers to control the rate at which they request elements. However, when there's a mismatch in the speed between the producer and the consumer, buffering can help balance their interaction. The `Stream.buffer` operator is designed to manage this, allowing the producer to keep working even if the consumer is slower. You can set a maximum buffer capacity using the `capacity` option. ### buffer The `Stream.buffer` operator queues elements to allow the producer to work independently from the consumer, up to a specified capacity. This helps when a faster producer and a slower consumer need to operate smoothly without blocking each other. **Example** (Using a Buffer to Handle Speed Mismatch) ```ts twoslash import { Stream, Console, Schedule, Effect } from "effect" const stream = Stream.range(1, 10).pipe( // Log each element before buffering Stream.tap((n) => Console.log(`before buffering: ${n}`)), // Buffer with a capacity of 4 elements Stream.buffer({ capacity: 4 }), // Log each element after buffering Stream.tap((n) => Console.log(`after buffering: ${n}`)), // Add a 5-second delay between each emission Stream.schedule(Schedule.spaced("5 seconds")) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: before buffering: 1 before buffering: 2 before buffering: 3 before buffering: 4 before buffering: 5 before buffering: 6 after buffering: 1 after buffering: 2 before buffering: 7 after buffering: 3 before buffering: 8 after buffering: 4 before buffering: 9 after buffering: 5 before buffering: 10 ... */ ``` Different buffering options let you tailor the buffering strategy based on your use case: | **Buffering Type** | **Configuration** | **Description** | | ------------------- | -------------------------------------------- | ------------------------------------------------------------- | | **Bounded Queue** | `{ capacity: number }` | Limits the queue to a fixed size. | | **Unbounded Queue** | `{ capacity: "unbounded" }` | Allows an unlimited number of buffered items. | | **Sliding Queue** | `{ capacity: number, strategy: "sliding" }` | Keeps the most recent items, discarding older ones when full. | | **Dropping Queue** | `{ capacity: number, strategy: "dropping" }` | Keeps the earliest items, discarding new ones when full. | ## Debouncing Debouncing is a technique used to prevent a function from firing too frequently, which is particularly useful when a stream emits values rapidly but only the last value after a pause is needed. The `Stream.debounce` function achieves this by delaying the emission of values until a specified time period has passed without any new values. If a new value arrives during the waiting period, the timer resets, and only the latest value will eventually be emitted after a pause. **Example** (Debouncing a Stream of Rapidly Emitted Values) ```ts twoslash import { Stream, Effect } from "effect" // Helper function to log with elapsed time since the last log let last = Date.now() const log = (message: string) => Effect.sync(() => { const end = Date.now() console.log(`${message} after ${end - last}ms`) last = end }) const stream = Stream.make(1, 2, 3).pipe( // Emit the value 4 after 200 ms Stream.concat( Stream.fromEffect(Effect.sleep("200 millis").pipe(Effect.as(4))) ), // Continue with more rapid values Stream.concat(Stream.make(5, 6)), // Emit 7 after 150 ms Stream.concat( Stream.fromEffect(Effect.sleep("150 millis").pipe(Effect.as(7))) ), Stream.concat(Stream.make(8)), Stream.tap((n) => log(`Received ${n}`)), // Only emit values after a pause of at least 100 milliseconds Stream.debounce("100 millis"), Stream.tap((n) => log(`> Emitted ${n}`)) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Example Output: Received 1 after 5ms Received 2 after 2ms Received 3 after 0ms > Emitted 3 after 104ms Received 4 after 99ms Received 5 after 1ms Received 6 after 0ms > Emitted 6 after 101ms Received 7 after 50ms Received 8 after 1ms > Emitted 8 after 101ms { _id: 'Chunk', values: [ 3, 6, 8 ] } */ ``` ## Throttling Throttling is a technique for regulating the rate at which elements are emitted from a stream. It helps maintain a steady data output pace, which is valuable in situations where data processing needs to occur at a consistent rate. The `Stream.throttle` function uses the [token bucket algorithm](https://en.wikipedia.org/wiki/Token_bucket) to control the rate of stream emissions. **Example** (Throttle Configuration) ```ts showLineNumbers=false Stream.throttle({ cost: () => 1, duration: "100 millis", units: 1 }) ``` In this configuration: - Each chunk processed uses one token (`cost = () => 1`). - Tokens are replenished at a rate of one token (`units: 1`) every 100 milliseconds (`duration: "100 millis"`). ### Shape Strategy (Default) The "shape" strategy moderates data flow by delaying chunk emissions until they comply with specified bandwidth constraints. This strategy ensures that data throughput does not exceed defined limits, allowing for steady and controlled data emission. **Example** (Applying Throttling with the Shape Strategy) ```ts twoslash import { Stream, Effect, Schedule, Chunk } from "effect" // Helper function to log with elapsed time since last log let last = Date.now() const log = (message: string) => Effect.sync(() => { const end = Date.now() console.log(`${message} after ${end - last}ms`) last = end }) const stream = Stream.fromSchedule(Schedule.spaced("50 millis")).pipe( Stream.take(6), Stream.tap((n) => log(`Received ${n}`)), Stream.throttle({ cost: Chunk.size, duration: "100 millis", units: 1 }), Stream.tap((n) => log(`> Emitted ${n}`)) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Example Output: Received 0 after 56ms > Emitted 0 after 0ms Received 1 after 52ms > Emitted 1 after 48ms Received 2 after 52ms > Emitted 2 after 49ms Received 3 after 52ms > Emitted 3 after 48ms Received 4 after 52ms > Emitted 4 after 47ms Received 5 after 52ms > Emitted 5 after 49ms { _id: 'Chunk', values: [ 0, 1, 2, 3, 4, 5 ] } */ ``` ### Enforce Strategy The "enforce" strategy strictly regulates data flow by discarding chunks that exceed bandwidth constraints. **Example** (Throttling with the Enforce Strategy) ```ts twoslash import { Stream, Effect, Schedule, Chunk } from "effect" // Helper function to log with elapsed time since last log let last = Date.now() const log = (message: string) => Effect.sync(() => { const end = Date.now() console.log(`${message} after ${end - last}ms`) last = end }) const stream = Stream.make(1, 2, 3, 4, 5, 6).pipe( Stream.schedule(Schedule.exponential("100 millis")), Stream.tap((n) => log(`Received ${n}`)), Stream.throttle({ cost: Chunk.size, duration: "1 second", units: 1, strategy: "enforce" }), Stream.tap((n) => log(`> Emitted ${n}`)) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Example Output: Received 1 after 106ms > Emitted 1 after 1ms Received 2 after 200ms Received 3 after 402ms Received 4 after 801ms > Emitted 4 after 1ms Received 5 after 1601ms > Emitted 5 after 1ms Received 6 after 3201ms > Emitted 6 after 0ms { _id: 'Chunk', values: [ 1, 4, 5, 6 ] } */ ``` ### burst option The `Stream.throttle` function offers a burst option that allows for temporary increases in data throughput beyond the set rate limits. This option is set to greater than 0 to activate burst capability (default is 0, indicating no burst support). The burst capacity provides additional tokens in the token bucket, enabling the stream to momentarily exceed its configured rate when bursts of data occur. **Example** (Throttling with Burst Capacity) ```ts twoslash import { Effect, Schedule, Stream, Chunk } from "effect" // Helper function to log with elapsed time since last log let last = Date.now() const log = (message: string) => Effect.sync(() => { const end = Date.now() console.log(`${message} after ${end - last}ms`) last = end }) const stream = Stream.fromSchedule(Schedule.spaced("10 millis")).pipe( Stream.take(20), Stream.tap((n) => log(`Received ${n}`)), Stream.throttle({ cost: Chunk.size, duration: "200 millis", units: 5, strategy: "enforce", burst: 2 }), Stream.tap((n) => log(`> Emitted ${n}`)) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Example Output: Received 0 after 16ms > Emitted 0 after 0ms Received 1 after 12ms > Emitted 1 after 0ms Received 2 after 11ms > Emitted 2 after 0ms Received 3 after 11ms > Emitted 3 after 0ms Received 4 after 11ms > Emitted 4 after 1ms Received 5 after 11ms > Emitted 5 after 0ms Received 6 after 12ms > Emitted 6 after 0ms Received 7 after 11ms Received 8 after 12ms Received 9 after 11ms Received 10 after 11ms > Emitted 10 after 0ms Received 11 after 11ms Received 12 after 11ms Received 13 after 12ms > Emitted 13 after 0ms Received 14 after 11ms Received 15 after 12ms Received 16 after 11ms Received 17 after 11ms > Emitted 17 after 0ms Received 18 after 12ms Received 19 after 10ms { _id: 'Chunk', values: [ 0, 1, 2, 3, 4, 5, 6, 10, 13, 17 ] } */ ``` In this setup, the stream starts with a bucket containing 5 tokens, allowing the first five chunks to be emitted instantly. The additional burst capacity of 2 accommodates further emissions momentarily, allowing for handling of subsequent data more flexibly. Over time, as the bucket refills according to the throttle configuration, additional elements are emitted, demonstrating how the burst capability can manage uneven data flows effectively. ## Scheduling When working with streams, you may need to introduce specific time intervals between each element's emission. The `Stream.schedule` combinator allows you to set these intervals. **Example** (Adding a Delay Between Stream Emissions) ```ts twoslash import { Stream, Schedule, Console, Effect } from "effect" // Create a stream that emits values with a 1-second delay between each const stream = Stream.make(1, 2, 3, 4, 5).pipe( Stream.schedule(Schedule.spaced("1 second")), Stream.tap(Console.log) ) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: 1 2 3 4 5 { _id: "Chunk", values: [ 1, 2, 3, 4, 5 ] } */ ``` In this example, we've used the `Schedule.spaced("1 second")` schedule to introduce a one-second gap between each emission in the stream. # [Resourceful Streams](https://effect.website/docs/stream/resourceful-streams/) ## Overview In the Stream module, you'll find that most of the constructors offer a special variant designed for lifting a scoped resource into a `Stream`. When you use these specific constructors, you're essentially creating streams that are inherently safe with regards to resource management. These constructors, before creating the stream, handle the resource acquisition, and after the stream's usage, they ensure its proper closure. Stream also provides us with `Stream.acquireRelease` and `Stream.finalizer` constructors that share similarities with `Effect.acquireRelease` and `Effect.addFinalizer`. These tools empower us to perform cleanup or finalization tasks before the stream concludes its operation. ## Acquire Release In this section, we'll explore an example that demonstrates the use of `Stream.acquireRelease` when working with file operations. ```ts twoslash import { Stream, Console, Effect } from "effect" // Simulating File operations const open = (filename: string) => Effect.gen(function* () { yield* Console.log(`Opening ${filename}`) return { getLines: Effect.succeed(["Line 1", "Line 2", "Line 3"]), close: Console.log(`Closing ${filename}`) } }) const stream = Stream.acquireRelease( open("file.txt"), (file) => file.close ).pipe(Stream.flatMap((file) => file.getLines)) Effect.runPromise(Stream.runCollect(stream)).then(console.log) /* Output: Opening file.txt Closing file.txt { _id: "Chunk", values: [ [ "Line 1", "Line 2", "Line 3" ] ] } */ ``` In this code snippet, we're simulating file operations using the `open` function. The `Stream.acquireRelease` function is employed to ensure that the file is correctly opened and closed, and we then process the lines of the file using the acquired resource. ## Finalization In this section, we'll explore the concept of finalization in streams. Finalization allows us to execute a specific action before a stream ends. This can be particularly useful when we want to perform cleanup tasks or add final touches to a stream. Imagine a scenario where our streaming application needs to clean up a temporary directory when it completes its execution. We can achieve this using the `Stream.finalizer` function: ```ts twoslash import { Stream, Console, Effect } from "effect" const application = Stream.fromEffect(Console.log("Application Logic.")) const deleteDir = (dir: string) => Console.log(`Deleting dir: ${dir}`) const program = application.pipe( Stream.concat( Stream.finalizer( deleteDir("tmp").pipe( Effect.andThen(Console.log("Temporary directory was deleted.")) ) ) ) ) Effect.runPromise(Stream.runCollect(program)).then(console.log) /* Output: Application Logic. Deleting dir: tmp Temporary directory was deleted. { _id: "Chunk", values: [ undefined, undefined ] } */ ``` In this code example, we start with our application logic represented by the `application` stream. We then use `Stream.finalizer` to define a finalization step, which deletes a temporary directory and logs a message. This ensures that the temporary directory is cleaned up properly when the application completes its execution. ## Ensuring In this section, we'll explore a scenario where we need to perform actions after the finalization of a stream. To achieve this, we can utilize the `Stream.ensuring` operator. Consider a situation where our application has completed its primary logic and finalized some resources, but we also need to perform additional actions afterward. We can use `Stream.ensuring` for this purpose: ```ts twoslash import { Stream, Console, Effect } from "effect" const program = Stream.fromEffect(Console.log("Application Logic.")).pipe( Stream.concat(Stream.finalizer(Console.log("Finalizing the stream"))), Stream.ensuring( Console.log("Doing some other works after stream's finalization") ) ) Effect.runPromise(Stream.runCollect(program)).then(console.log) /* Output: Application Logic. Finalizing the stream Doing some other works after stream's finalization { _id: "Chunk", values: [ undefined, undefined ] } */ ``` In this code example, we start with our application logic represented by the `Application Logic.` message. We then use `Stream.finalizer` to specify the finalization step, which logs `Finalizing the stream`. After that, we use `Stream.ensuring` to indicate that we want to perform additional tasks after the stream's finalization, resulting in the message `Performing additional tasks after stream's finalization`. This ensures that our post-finalization actions are executed as expected. # [TestClock](https://effect.website/docs/testing/testclock/) ## Overview import { Aside } from "@astrojs/starlight/components" In most cases, we want our unit tests to run as quickly as possible. Waiting for real time to pass can slow down our tests significantly. Effect provides a handy tool called `TestClock` that allows us to **control time during testing**. This means we can efficiently and predictably test code that involves time without having to wait for the actual time to pass. ## How TestClock Works Imagine `TestClock` as a wall clock that only moves forward when we adjust it manually using the `TestClock.adjust` and `TestClock.setTime` functions. The clock time does not progress on its own. When we adjust the clock time, any effects scheduled to run at or before that time will execute. This allows us to simulate time passage in tests without waiting for real time. **Example** (Simulating a Timeout with TestClock) ```ts twoslash import { Effect, TestClock, Fiber, Option, TestContext } from "effect" import * as assert from "node:assert" const test = Effect.gen(function* () { // Create a fiber that sleeps for 5 minutes and then times out // after 1 minute const fiber = yield* Effect.sleep("5 minutes").pipe( Effect.timeoutTo({ duration: "1 minute", onSuccess: Option.some, onTimeout: () => Option.none() }), Effect.fork ) // Adjust the TestClock by 1 minute to simulate the passage of time yield* TestClock.adjust("1 minute") // Get the result of the fiber const result = yield* Fiber.join(fiber) // Check if the result is None, indicating a timeout assert.ok(Option.isNone(result)) }).pipe(Effect.provide(TestContext.TestContext)) Effect.runPromise(test) ``` A key point is forking the fiber where `Effect.sleep` is invoked. Calls to `Effect.sleep` and related methods wait until the clock time matches or exceeds the scheduled time for their execution. By forking the fiber, we retain control over the clock time adjustments. ## Testing Recurring Effects Here's an example demonstrating how to test an effect that runs at fixed intervals using the `TestClock`: **Example** (Testing an Effect with Fixed Intervals) In this example, we test an effect that runs at regular intervals. An unbounded queue is used to manage the effects, and we verify the following: 1. No effect occurs before the specified recurrence period. 2. An effect occurs after the recurrence period. 3. The effect executes exactly once. ```ts twoslash import { Effect, Queue, TestClock, Option, TestContext } from "effect" import * as assert from "node:assert" const test = Effect.gen(function* () { const q = yield* Queue.unbounded() yield* Queue.offer(q, undefined).pipe( // Delay the effect for 60 minutes and repeat it forever Effect.delay("60 minutes"), Effect.forever, Effect.fork ) // Check if no effect is performed before the recurrence period const a = yield* Queue.poll(q).pipe(Effect.andThen(Option.isNone)) // Adjust the TestClock by 60 minutes to simulate the passage of time yield* TestClock.adjust("60 minutes") // Check if an effect is performed after the recurrence period const b = yield* Queue.take(q).pipe(Effect.as(true)) // Check if the effect is performed exactly once const c = yield* Queue.poll(q).pipe(Effect.andThen(Option.isNone)) // Adjust the TestClock by another 60 minutes yield* TestClock.adjust("60 minutes") // Check if another effect is performed const d = yield* Queue.take(q).pipe(Effect.as(true)) const e = yield* Queue.poll(q).pipe(Effect.andThen(Option.isNone)) // Ensure that all conditions are met assert.ok(a && b && c && d && e) }).pipe(Effect.provide(TestContext.TestContext)) Effect.runPromise(test) ``` It's important to note that after each recurrence, the next occurrence is scheduled to happen at the appropriate time. Adjusting the clock by 60 minutes places exactly one value in the queue; adjusting by another 60 minutes adds another value. ## Testing Clock This example demonstrates how to test the behavior of the `Clock` using the `TestClock`: **Example** (Simulating Time Passage with TestClock) ```ts twoslash import { Effect, Clock, TestClock, TestContext } from "effect" import * as assert from "node:assert" const test = Effect.gen(function* () { // Get the current time using the Clock const startTime = yield* Clock.currentTimeMillis // Adjust the TestClock by 1 minute to simulate the passage of time yield* TestClock.adjust("1 minute") // Get the current time again const endTime = yield* Clock.currentTimeMillis // Check if the time difference is at least // 60,000 milliseconds (1 minute) assert.ok(endTime - startTime >= 60_000) }).pipe(Effect.provide(TestContext.TestContext)) Effect.runPromise(test) ``` ## Testing Deferred The `TestClock` also impacts asynchronous code scheduled to run after a specific time. **Example** (Simulating Delayed Execution with Deferred and TestClock) ```ts twoslash import { Effect, Deferred, TestClock, TestContext } from "effect" import * as assert from "node:assert" const test = Effect.gen(function* () { // Create a deferred value const deferred = yield* Deferred.make() // Run two effects concurrently: sleep for 10 seconds and succeed // the deferred with a value of 1 yield* Effect.all( [Effect.sleep("10 seconds"), Deferred.succeed(deferred, 1)], { concurrency: "unbounded" } ).pipe(Effect.fork) // Adjust the TestClock by 10 seconds yield* TestClock.adjust("10 seconds") // Await the value from the deferred const readRef = yield* Deferred.await(deferred) // Verify the deferred value is correctly set assert.ok(readRef === 1) }).pipe(Effect.provide(TestContext.TestContext)) Effect.runPromise(test) ``` # [Equal](https://effect.website/docs/trait/equal/) ## Overview The Equal module provides a simple and convenient way to define and check for equality between two values in TypeScript. Here are some key reasons why Effect exports an Equal module: 1. **Value-Based Equality**: JavaScript's native equality operators (`===` and `==`) check for equality by reference, meaning they compare objects based on their memory addresses rather than their content. This behavior can be problematic when you want to compare objects with the same values but different references. The Equal module offers a solution by allowing developers to define custom equality checks based on the values of objects. 2. **Custom Equality**: The Equal module enables developers to implement custom equality checks for their data types and classes. This is crucial when you have specific requirements for determining when two objects should be considered equal. By implementing the `Equal` interface, developers can define their own equality logic. 3. **Data Integrity**: In some applications, maintaining data integrity is crucial. The ability to perform value-based equality checks ensures that identical data is not duplicated within collections like sets or maps. This can lead to more efficient memory usage and more predictable behavior. 4. **Predictable Behavior**: The Equal module promotes more predictable behavior when comparing objects. By explicitly defining equality criteria, developers can avoid unexpected results that may occur with JavaScript's default reference-based equality checks. ## How to Perform Equality Checking in Effect In Effect it's advisable to **stop using** JavaScript's `===` and `==` operators and instead rely on the `Equal.equals` function. This function can work with any data type that implements the `Equal` interface. Some examples of such data types include [Option](/docs/data-types/option/), [Either](/docs/data-types/either/), [HashSet](https://effect-ts.github.io/effect/effect/HashSet.ts.html), and [HashMap](https://effect-ts.github.io/effect/effect/HashMap.ts.html). When you use `Equal.equals` and your objects do not implement the `Equal` interface, it defaults to using the `===` operator for object comparison: **Example** (Using `Equal.equals` with Default Comparison) ```ts twoslash import { Equal } from "effect" // Two objects with identical properties and values const a = { name: "Alice", age: 30 } const b = { name: "Alice", age: 30 } // Equal.equals falls back to the default '===' comparison console.log(Equal.equals(a, b)) // Output: false ``` In this example, `a` and `b` are two separate objects with the same contents. However, `===` considers them different because they occupy different memory locations. This behavior can lead to unexpected results when you want to compare values based on their content. However, you can configure your models to ensure that `Equal.equals` behaves consistently with your custom equality checks. There are two alternative approaches: 1. **Implementing the `Equal` Interface**: This method is useful when you need to define your custom equality check. 2. **Using the Data Module**: For simple value equality, the [Data](/docs/data-types/data/) module provides a more straightforward solution by automatically generating default implementations for `Equal`. Let's explore both. ### Implementing the Equal Interface To create custom equality behavior, you can implement the `Equal` interface in your models. This interface extends the `Hash` interface from the [Hash](/docs/trait/hash/) module. **Example** (Implementing `Equal` and `Hash` for a Custom Class) ```ts twoslash import { Equal, Hash } from "effect" class Person implements Equal.Equal { constructor( readonly id: number, // Unique identifier readonly name: string, readonly age: number ) {} // Define equality based on id, name, and age [Equal.symbol](that: Equal.Equal): boolean { if (that instanceof Person) { return ( Equal.equals(this.id, that.id) && Equal.equals(this.name, that.name) && Equal.equals(this.age, that.age) ) } return false } // Generate a hash code based on the unique id [Hash.symbol](): number { return Hash.hash(this.id) } } ``` In the above code, we define a custom equality function `[Equal.symbol]` and a hash function `[Hash.symbol]` for the `Person` class. The `Hash` interface optimizes equality checks by comparing hash values instead of the objects themselves. When you use the `Equal.equals` function to compare two objects, it first checks if their hash values are equal. If not, it quickly determines that the objects are not equal, avoiding the need for a detailed property-by-property comparison. Once you've implemented the `Equal` interface, you can utilize the `Equal.equals` function to check for equality using your custom logic. **Example** (Comparing `Person` Instances) ```ts twoslash collapse={3-26} import { Equal, Hash } from "effect" class Person implements Equal.Equal { constructor( readonly id: number, // Unique identifier for each person readonly name: string, readonly age: number ) {} // Defines equality based on id, name, and age [Equal.symbol](that: Equal.Equal): boolean { if (that instanceof Person) { return ( Equal.equals(this.id, that.id) && Equal.equals(this.name, that.name) && Equal.equals(this.age, that.age) ) } return false } // Generates a hash code based primarily on the unique id [Hash.symbol](): number { return Hash.hash(this.id) } } const alice = new Person(1, "Alice", 30) console.log(Equal.equals(alice, new Person(1, "Alice", 30))) // Output: true const bob = new Person(2, "Bob", 40) console.log(Equal.equals(alice, bob)) // Output: false ``` In this code, the equality check returns `true` when comparing `alice` to a new `Person` object with identical property values and `false` when comparing `alice` to `bob` due to their differing property values. ### Simplifying Equality with the Data Module Implementing both `Equal` and `Hash` can become cumbersome when all you need is straightforward value equality checks. Luckily, the [Data](/docs/data-types/data/) module provides a simpler solution. It offers APIs that automatically generate default implementations for both `Equal` and `Hash`. **Example** (Using `Data.struct` for Equality Checks) ```ts twoslash import { Equal, Data } from "effect" const alice = Data.struct({ id: 1, name: "Alice", age: 30 }) const bob = Data.struct({ id: 2, name: "Bob", age: 40 }) console.log( Equal.equals(alice, Data.struct({ id: 1, name: "Alice", age: 30 })) ) // Output: true console.log(Equal.equals(alice, { id: 1, name: "Alice", age: 30 })) // Output: false console.log(Equal.equals(alice, bob)) // Output: false ``` In this example, we use the [Data.struct](/docs/data-types/data/#struct) function to create structured data objects and check their equality using `Equal.equals`. The Data module simplifies the process by providing a default implementation for both `Equal` and `Hash`, allowing you to focus on comparing values without the need for explicit implementations. The Data module isn't limited to just structs. It can handle various data types, including tuples, arrays, and records. If you're curious about how to leverage its full range of features, you can explore the [Data module documentation](/docs/data-types/data/#value-equality). ## Working with Collections JavaScript's built-in `Set` and `Map` can be a bit tricky when it comes to checking equality: **Example** (Native `Set` with Reference-Based Equality) ```ts twoslash const set = new Set() // Adding two objects with the same content to the set set.add({ name: "Alice", age: 30 }) set.add({ name: "Alice", age: 30 }) // Even though the objects have identical values, they are treated // as different elements because JavaScript compares objects by reference, // not by value. console.log(set.size) // Output: 2 ``` Even though the two elements in the set have the same values, the set contains two elements. Why? JavaScript's `Set` checks for equality by reference, not by values. To perform value-based equality checks, you'll need to use the `Hash*` collection types available in the `effect` package. These collection types, such as [HashSet](https://effect-ts.github.io/effect/effect/HashSet.ts.html) and [HashMap](https://effect-ts.github.io/effect/effect/HashMap.ts.html), provide support for the `Equal` interface. ### HashSet When you use the `HashSet`, it correctly handles value-based equality checks. In the following example, even though you're adding two objects with the same values, the `HashSet` treats them as a single element. **Example** (Using `HashSet` for Value-Based Equality) ```ts twoslash import { HashSet, Data } from "effect" // Creating a HashSet with objects that implement the Equal interface const set = HashSet.empty().pipe( HashSet.add(Data.struct({ name: "Alice", age: 30 })), HashSet.add(Data.struct({ name: "Alice", age: 30 })) ) // HashSet recognizes them as equal, so only one element is stored console.log(HashSet.size(set)) // Output: 1 ``` **Note**: It's crucial to use elements that implement the `Equal` interface, either by implementing custom equality checks or by using the Data module. This ensures proper functionality when working with `HashSet`. Without this, you'll encounter the same behavior as the native `Set` data type: **Example** (Reference-Based Equality in `HashSet`) ```ts twoslash import { HashSet } from "effect" // Creating a HashSet with objects that do NOT implement // the Equal interface const set = HashSet.empty().pipe( HashSet.add({ name: "Alice", age: 30 }), HashSet.add({ name: "Alice", age: 30 }) ) // Since these objects are compared by reference, // HashSet considers them different console.log(HashSet.size(set)) // Output: 2 ``` In this case, without using the Data module alongside `HashSet`, you'll experience the same behavior as the native `Set` data type. The set contains two elements because it checks for equality by reference, not by values. ### HashMap When working with the `HashMap`, you have the advantage of comparing keys by their values instead of their references. This is particularly helpful in scenarios where you want to associate values with keys based on their content. **Example** (Value-Based Key Comparisons with `HashMap`) ```ts twoslash import { HashMap, Data } from "effect" // Adding two objects with identical values as keys const map = HashMap.empty().pipe( HashMap.set(Data.struct({ name: "Alice", age: 30 }), 1), HashMap.set(Data.struct({ name: "Alice", age: 30 }), 2) ) console.log(HashMap.size(map)) // Output: 1 // Retrieve the value associated with a key console.log(HashMap.get(map, Data.struct({ name: "Alice", age: 30 }))) /* Output: { _id: 'Option', _tag: 'Some', value: 2 } */ ``` In this code snippet, `HashMap` is used to create a map where the keys are objects constructed with `Data.struct`. These objects contain identical values, which would usually create separate entries in a regular JavaScript `Map` because the default comparison is reference-based. `HashMap`, however, uses value-based comparison, meaning the two objects with identical content are treated as the same key. Thus, when we add both objects, the second key-value pair overrides the first, resulting in a single entry in the map. # [Hash](https://effect.website/docs/trait/hash/) ## Overview The `Hash` interface is closely tied to the [Equal](/docs/trait/equal/) interface and serves a supportive role in optimizing equality checks by providing a mechanism for hashing. Hashing is an important step in the efficient determination of equality between two values, particularly when used with data structures like hash tables. ## Role of Hash in Equality Checking The primary purpose of the `Hash` interface is to provide a quick and efficient way to determine if two values are definitely not equal, thereby complementing the [Equal](/docs/trait/equal/) interface. When two values implement the [Equal](/docs/trait/equal/) interface, their hash values (computed using the `Hash` interface) are compared first: - **Different Hash Values**: If the hash values are different, it is guaranteed that the values themselves are different. This quick check allows the system to avoid a potentially expensive equality check. - **Same Hash Values**: If the hash values are the same, it does not guarantee that the values are equal, only that they might be. In this case, a more thorough comparison using the [Equal](/docs/trait/equal/) interface is performed to determine actual equality. This method dramatically speeds up the equality checking process, especially in collections where quick look-up and insertion times are crucial, such as in hash sets or hash maps. ## Implementing the Hash Interface Consider a scenario where you have a custom `Person` class, and you want to check if two instances are equal based on their properties. By implementing both the `Equal` and `Hash` interfaces, you can efficiently manage these checks: **Example** (Implementing `Equal` and `Hash` for a Custom Class) ```ts twoslash import { Equal, Hash } from "effect" class Person implements Equal.Equal { constructor( readonly id: number, // Unique identifier readonly name: string, readonly age: number ) {} // Define equality based on id, name, and age [Equal.symbol](that: Equal.Equal): boolean { if (that instanceof Person) { return ( Equal.equals(this.id, that.id) && Equal.equals(this.name, that.name) && Equal.equals(this.age, that.age) ) } return false } // Generate a hash code based on the unique id [Hash.symbol](): number { return Hash.hash(this.id) } } const alice = new Person(1, "Alice", 30) console.log(Equal.equals(alice, new Person(1, "Alice", 30))) // Output: true const bob = new Person(2, "Bob", 40) console.log(Equal.equals(alice, bob)) // Output: false ``` Explanation: - The `[Equal.symbol]` method determines equality by comparing the `id`, `name`, and `age` fields of `Person` instances. This approach ensures that the equality check is comprehensive and considers all relevant attributes. - The `[Hash.symbol]` method computes a hash code using the `id` of the person. This value is used to quickly differentiate between instances in hashing operations, optimizing the performance of data structures that utilize hashing. - The equality check returns `true` when comparing `alice` to a new `Person` object with identical property values and `false` when comparing `alice` to `bob` due to their differing property values.