The Micro module in Effect 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, Ref, Queue, and 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:
Micro is a component of the Effect library and can be imported similarly to any other module in your TypeScript project:
This import statement allows you to access all functionalities of Micro, enabling you to use its features in your application.
The Micro Type
The Micro type uses three type parameters:
which mirror those of the Effect type:
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 designed to capture the outcome of a Micro computation. It uses the Either data type to distinguish between successful outcomes and failures:
The MicroCause Type
The MicroCause type represents the possible causes of an effect’s failure.
MicroCause consists of three specific types:
Failure Type
Description
Die
Indicates an unforeseen defect that wasn’t planned for in the system’s logic.
Fail<E>
Covers anticipated errors that are recognized and typically handled within the application.
Interrupt
Signifies an operation that has been purposefully stopped.
Tutorial: Wrapping a Promise-based API with Micro
In this tutorial, we’ll demonstrate how to wrap a Promise-based API using the Micro library from Effect. We’ll use a simple example where we interact with a hypothetical weather forecasting API. The goal is to encapsulate the asynchronous API call within Micro’s structured error handling and execution flow.
Step 1: Create a Promise-based API Function
First, let’s define a simple Promise-based function that simulates fetching weather data from an external service.
Step 2: Wrap the Promise with Micro
Next, we’ll wrap our fetchWeather function using Micro to handle both successful and failed Promise outcomes.
Here, Micro.promise is used to convert the Promise returned by fetchWeather into a Micro effect.
Step 3: Running the Micro Effect
After wrapping our function, we need to execute the Micro effect and handle the results.
In this snippet, Micro.runPromise is used to execute the weatherEffect.
It converts the Micro effect back into a Promise, making it easier to integrate with other Promise-based code or simply to manage asynchronous operations in a familiar way.
You can also use Micro.runPromiseExit to get more detailed information about the effect’s exit status:
Step 4: Adding Error Handling
To further enhance the function, you might want to handle specific errors differently.
Micro provides methods like Micro.tryPromise to handle anticipated errors gracefully.
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.
either
The Micro.either function transforms an Micro<A, E, R> into an effect that encapsulates both potential failure and success within an Either data type.
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:
catchAll
The Micro.catchAll function allows you to catch any error that occurs in the program and provide a fallback.
catchTag
If your program’s errors are all tagged with a _tag field that acts as a discriminator (recommended) you can use the Effect.catchTag function to catch and handle specific errors with precision.
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
orDie
catchAllDefect
Fallback
orElseSucceed
The Micro.orElseSucceed function will always replace the original failure with a success value, so the resulting effect cannot fail:
Matching
match
matchEffect
matchCause / matchCauseEffect
Retrying
To demonstrate the functionality of the Micro.retry function, we will be working with the following helper that simulates an effect with possible failures:
retry
Sandboxing
The Micro.sandbox function allows you to encapsulate all the potential causes of an error in an effect.
It exposes the full MicroCause of an effect, whether it’s due to a failure, fiber interruption, or defect.
Inspecting Errors
tapError
Executes an effectful operation to inspect the failure of an effect without altering it.
tapErrorCause
Inspects the underlying cause of an effect’s failure.
tapDefect
Specifically inspects non-recoverable failures or defects in an effect.
Yieldable Errors
“Yieldable Errors” are special types of errors that can be yielded within a generator function used by Micro.gen.
The unique feature of these errors is that you don’t need to use the Micro.fail API explicitly to handle them.
They offer a more intuitive and convenient way to work with custom errors in your code.
Error
TaggedError
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.
Now that we have our service tag defined, let’s see how we can use it by building a simple program.
It’s worth noting that the type of the program variable includes Random in the Requirements type parameter: Micro<void, never, Random>.
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.
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, which represent the release of a resource.
Close the scope, releasing all acquired resources and executing any added finalizers.
By default, when a MicroScope is closed, all finalizers added to that MicroScope are executed in the reverse order in which they were added. This approach makes sense because releasing resources in the reverse order of acquisition ensures that resources are properly closed.
For instance, if you open a network connection and then access a file on a remote server, you must close the file before closing the network connection. This sequence is critical to maintaining the ability to interact with the remote server.
addFinalizer
The Micro.addFinalizer function provides a higher-level API for adding finalizers to the scope of a Micro value.
These finalizers are guaranteed to execute when the associated scope is closed, and their behavior may depend on the MicroExit value with which the scope is closed.
Let’s observe how things behave in the event of success:
Next, let’s explore how things behave in the event of a failure:
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.
For example, let’s define a simple resource:
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.
Here’s an example that demonstrates the usage of acquireUseRelease:
Scheduling
repeat
The Micro.repeat function returns a new effect that repeats the given effect according to a specified schedule or until the first failure.
Success Example
Failure Example
helper
To demonstrate the functionality of different schedules, we will be working with the following helper:
scheduleSpaced
A schedule that recurs continuously, each repetition spaced the specified duration from the last run.
scheduleExponential
A schedule that recurs using exponential backoff.
scheduleUnion
Combines two schedules through union, by recurring if either schedule wants to recur, using the minimum of the two delays between recurrences.
scheduleIntersect
Combines two schedules through the intersection, by recurring only if both schedules want to recur, using the maximum of the two delays between recurrences.
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:
Joining Fibers
A common operation with fibers is joining them using the .join property.
This property returns a Micro that will succeed or fail based on the outcome of the fiber it joins:
Awaiting Fibers
Another useful property for fibers is .await.
This property returns an effect containing a MicroExit value, which provides detailed information about how the fiber completed.
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.
Racing
The Micro.race function lets you race multiple effects concurrently and returns the result of the first one that successfully completes.
If you need to handle the first effect to complete, whether it succeeds or fails, you can use the Micro.either function.
Timing out
Interruptible Operation: If the operation can be interrupted, it is terminated immediately once the timeout threshold is reached, resulting in a TimeoutException.
Uninterruptible Operation: If the operation is uninterruptible, it continues until completion before the TimeoutException is assessed.