Getting Started with Micro
On this page
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:
bash
npm install effect
bash
npm install effect
Micro is a component of the Effect library and can be imported similarly to any other module in your TypeScript project:
ts
import * asMicro from "effect/Micro"
ts
import * asMicro from "effect/Micro"
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:
ts
Micro<Success, Error, Requirements>
ts
Micro<Success, Error, Requirements>
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 isnever
, 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 typenever
. - 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 isnever
, it means the effect has no requirements and theContext
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:
ts
type MicroExit<A, E = never> = Either<A, MicroCause<E>>
ts
type MicroExit<A, E = never> = Either<A, MicroCause<E>>
The MicroCause Type
The MicroCause
type represents the possible causes of an effect's failure.
MicroCause
consists of three specific types:
ts
type MicroCause<E> = Die | Fail<E> | Interrupt
ts
type MicroCause<E> = Die | Fail<E> | Interrupt
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.
ts
functionfetchWeather (city : string):Promise <string> {return newPromise ((resolve ,reject ) => {setTimeout (() => {if (city === "London") {resolve ("Sunny")} else {reject (newError ("Weather data not found for this location"))}}, 1_000)})}
ts
functionfetchWeather (city : string):Promise <string> {return newPromise ((resolve ,reject ) => {setTimeout (() => {if (city === "London") {resolve ("Sunny")} else {reject (newError ("Weather data not found for this location"))}}, 1_000)})}
Step 2: Wrap the Promise with Micro
Next, we'll wrap our fetchWeather
function using Micro to handle both successful and failed Promise outcomes.
ts
functiongetWeather (city : string) {returnMicro .promise (() =>fetchWeather (city ))}
ts
functiongetWeather (city : string) {returnMicro .promise (() =>fetchWeather (city ))}
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.
ts
constweatherEffect =getWeather ("London")Micro .runPromise (weatherEffect ).then ((result ) =>console .log (`The weather in London is: ${result }`)).catch ((error ) =>console .error (`Failed to fetch weather data: ${error .message }`))/*Output:The weather in London is: Sunny*/
ts
constweatherEffect =getWeather ("London")Micro .runPromise (weatherEffect ).then ((result ) =>console .log (`The weather in London is: ${result }`)).catch ((error ) =>console .error (`Failed to fetch weather data: ${error .message }`))/*Output:The weather in London is: Sunny*/
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:
ts
Micro .runPromiseExit (weatherEffect ).then ((exit ) =>console .log (exit ))/*Output:{ _id: 'Either', _tag: 'Right', right: 'Sunny' }*/
ts
Micro .runPromiseExit (weatherEffect ).then ((exit ) =>console .log (exit ))/*Output:{ _id: 'Either', _tag: 'Right', right: 'Sunny' }*/
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.
ts
classWeatherError {readonly_tag = "WeatherError"constructor(readonlymessage : string) {}}functiongetWeather (city : string) {returnMicro .tryPromise ({try : () =>fetchWeather (city ),// remap the errorcatch : (error ) => newWeatherError (String (error ))})}constweatherEffect =getWeather ("Paris")Micro .runPromise (weatherEffect ).then ((result ) =>console .log (`The weather in London is: ${result }`)).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"}*/
ts
classWeatherError {readonly_tag = "WeatherError"constructor(readonlymessage : string) {}}functiongetWeather (city : string) {returnMicro .tryPromise ({try : () =>fetchWeather (city ),// remap the errorcatch : (error ) => newWeatherError (String (error ))})}constweatherEffect =getWeather ("Paris")Micro .runPromise (weatherEffect ).then ((result ) =>console .log (`The weather in London is: ${result }`)).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.
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:
ts
import * asEither from "effect/Either"import * asMicro from "effect/Micro"classNetworkError {readonly_tag = "NetworkError"}classValidationError {readonly_tag = "ValidationError"}consttask =Micro .gen (function* () {// Simulate network and validation errorsif (Math .random () > 0.5) yield*Micro .fail (newNetworkError ())if (Math .random () > 0.5) yield*Micro .fail (newValidationError ())return "Success"})constrecovered =Micro .gen (function* () {constfailureOrSuccess = yield*Micro .either (task )returnEither .match (failureOrSuccess , {onLeft : (error ) => `Recovering from ${error ._tag }`,onRight : (value ) => `Result is: ${value }`})})Micro .runPromiseExit (recovered ).then (console .log )/*Example Output:{_id: 'Either',_tag: 'Right',right: 'Recovering from ValidationError'}*/
ts
import * asEither from "effect/Either"import * asMicro from "effect/Micro"classNetworkError {readonly_tag = "NetworkError"}classValidationError {readonly_tag = "ValidationError"}consttask =Micro .gen (function* () {// Simulate network and validation errorsif (Math .random () > 0.5) yield*Micro .fail (newNetworkError ())if (Math .random () > 0.5) yield*Micro .fail (newValidationError ())return "Success"})constrecovered =Micro .gen (function* () {constfailureOrSuccess = yield*Micro .either (task )returnEither .match (failureOrSuccess , {onLeft : (error ) => `Recovering from ${error ._tag }`,onRight : (value ) => `Result is: ${value }`})})Micro .runPromiseExit (recovered ).then (console .log )/*Example Output:{_id: 'Either',_tag: 'Right',right: 'Recovering from ValidationError'}*/
catchAll
The Micro.catchAll
function allows you to catch any error that occurs in the program and provide a fallback.
ts
import * asMicro from "effect/Micro"classNetworkError {readonly_tag = "NetworkError"}classValidationError {readonly_tag = "ValidationError"}consttask =Micro .gen (function* () {// Simulate network and validation errorsif (Math .random () > 0.5) yield*Micro .fail (newNetworkError ())if (Math .random () > 0.5) yield*Micro .fail (newValidationError ())return "Success"})constrecovered =task .pipe (Micro .catchAll ((error ) =>Micro .succeed (`Recovering from ${error ._tag }`)))Micro .runPromiseExit (recovered ).then (console .log )/*Example Output:{ _id: 'Either', _tag: 'Right', right: 'Recovering from NetworkError' }*/
ts
import * asMicro from "effect/Micro"classNetworkError {readonly_tag = "NetworkError"}classValidationError {readonly_tag = "ValidationError"}consttask =Micro .gen (function* () {// Simulate network and validation errorsif (Math .random () > 0.5) yield*Micro .fail (newNetworkError ())if (Math .random () > 0.5) yield*Micro .fail (newValidationError ())return "Success"})constrecovered =task .pipe (Micro .catchAll ((error ) =>Micro .succeed (`Recovering from ${error ._tag }`)))Micro .runPromiseExit (recovered ).then (console .log )/*Example Output:{ _id: 'Either', _tag: 'Right', right: 'Recovering from NetworkError' }*/
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.
ts
import * asMicro from "effect/Micro"classNetworkError {readonly_tag = "NetworkError"}classValidationError {readonly_tag = "ValidationError"}consttask =Micro .gen (function* () {// Simulate network and validation errorsif (Math .random () > 0.5) yield*Micro .fail (newNetworkError ())if (Math .random () > 0.5) yield*Micro .fail (newValidationError ())return "Success"})constrecovered =task .pipe (Micro .catchTag ("ValidationError", (_error ) =>Micro .succeed ("Recovering from ValidationError")))Micro .runPromiseExit (recovered ).then (console .log )/*Example Output:{_id: 'Either',_tag: 'Right',right: 'Recovering from ValidationError'}*/
ts
import * asMicro from "effect/Micro"classNetworkError {readonly_tag = "NetworkError"}classValidationError {readonly_tag = "ValidationError"}consttask =Micro .gen (function* () {// Simulate network and validation errorsif (Math .random () > 0.5) yield*Micro .fail (newNetworkError ())if (Math .random () > 0.5) yield*Micro .fail (newValidationError ())return "Success"})constrecovered =task .pipe (Micro .catchTag ("ValidationError", (_error ) =>Micro .succeed ("Recovering from ValidationError")))Micro .runPromiseExit (recovered ).then (console .log )/*Example Output:{_id: 'Either',_tag: 'Right',right: 'Recovering from ValidationError'}*/
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
ts
import * asMicro from "effect/Micro"constdivide = (a : number,b : number):Micro .Micro <number> =>b === 0?Micro .die (newError ("Cannot divide by zero")):Micro .succeed (a /b )Micro .runSync (divide (1, 0)) // throws Error: Cannot divide by zero
ts
import * asMicro from "effect/Micro"constdivide = (a : number,b : number):Micro .Micro <number> =>b === 0?Micro .die (newError ("Cannot divide by zero")):Micro .succeed (a /b )Micro .runSync (divide (1, 0)) // throws Error: Cannot divide by zero
orDie
ts
import * asMicro from "effect/Micro"constdivide = (a : number,b : number):Micro .Micro <number,Error > =>b === 0?Micro .fail (newError ("Cannot divide by zero")):Micro .succeed (a /b )constprogram =Micro .orDie (divide (1, 0))Micro .runSync (program ) // throws Error: Cannot divide by zero
ts
import * asMicro from "effect/Micro"constdivide = (a : number,b : number):Micro .Micro <number,Error > =>b === 0?Micro .fail (newError ("Cannot divide by zero")):Micro .succeed (a /b )constprogram =Micro .orDie (divide (1, 0))Micro .runSync (program ) // throws Error: Cannot divide by zero
catchAllDefect
ts
import * asMicro from "effect/Micro"constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))constprogram =Micro .catchAllDefect (Micro .die ("Boom!"), // Simulating a runtime error(defect ) =>consoleLog (`Unknown defect caught: ${defect }`))// We get an Either.Right because we caught all defectsMicro .runPromiseExit (program ).then (console .log )/*Output:Unknown defect caught: Boom!{ _id: 'Either', _tag: 'Right', right: undefined }*/
ts
import * asMicro from "effect/Micro"constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))constprogram =Micro .catchAllDefect (Micro .die ("Boom!"), // Simulating a runtime error(defect ) =>consoleLog (`Unknown defect caught: ${defect }`))// We get an Either.Right because we caught all defectsMicro .runPromiseExit (program ).then (console .log )/*Output:Unknown defect caught: Boom!{ _id: 'Either', _tag: 'Right', right: undefined }*/
Fallback
orElseSucceed
The Micro.orElseSucceed
function will always replace the original failure with a success value, so the resulting effect cannot fail:
ts
import * asMicro from "effect/Micro"classNegativeAgeError {readonly_tag = "NegativeAgeError"constructor(readonlyage : number) {}}classIllegalAgeError {readonly_tag = "IllegalAgeError"constructor(readonlyage : number) {}}constvalidate = (age : number):Micro .Micro <number,NegativeAgeError |IllegalAgeError > => {if (age < 0) {returnMicro .fail (newNegativeAgeError (age ))} else if (age < 18) {returnMicro .fail (newIllegalAgeError (age ))} else {returnMicro .succeed (age )}}constprogram =Micro .orElseSucceed (validate (3), () => 0)
ts
import * asMicro from "effect/Micro"classNegativeAgeError {readonly_tag = "NegativeAgeError"constructor(readonlyage : number) {}}classIllegalAgeError {readonly_tag = "IllegalAgeError"constructor(readonlyage : number) {}}constvalidate = (age : number):Micro .Micro <number,NegativeAgeError |IllegalAgeError > => {if (age < 0) {returnMicro .fail (newNegativeAgeError (age ))} else if (age < 18) {returnMicro .fail (newIllegalAgeError (age ))} else {returnMicro .succeed (age )}}constprogram =Micro .orElseSucceed (validate (3), () => 0)
Matching
match
ts
import * asMicro from "effect/Micro"constsuccess :Micro .Micro <number,Error > =Micro .succeed (42)constfailure :Micro .Micro <number,Error > =Micro .fail (newError ("Uh oh!"))constprogram1 =Micro .match (success , {onFailure : (error ) => `failure: ${error .message }`,onSuccess : (value ) => `success: ${value }`})Micro .runPromise (program1 ).then (console .log ) // Output: "success: 42"constprogram2 =Micro .match (failure , {onFailure : (error ) => `failure: ${error .message }`,onSuccess : (value ) => `success: ${value }`})Micro .runPromise (program2 ).then (console .log ) // Output: "failure: Uh oh!"
ts
import * asMicro from "effect/Micro"constsuccess :Micro .Micro <number,Error > =Micro .succeed (42)constfailure :Micro .Micro <number,Error > =Micro .fail (newError ("Uh oh!"))constprogram1 =Micro .match (success , {onFailure : (error ) => `failure: ${error .message }`,onSuccess : (value ) => `success: ${value }`})Micro .runPromise (program1 ).then (console .log ) // Output: "success: 42"constprogram2 =Micro .match (failure , {onFailure : (error ) => `failure: ${error .message }`,onSuccess : (value ) => `success: ${value }`})Micro .runPromise (program2 ).then (console .log ) // Output: "failure: Uh oh!"
matchEffect
ts
import * asMicro from "effect/Micro"constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))constsuccess :Micro .Micro <number,Error > =Micro .succeed (42)constfailure :Micro .Micro <number,Error > =Micro .fail (newError ("Uh oh!"))constprogram1 =Micro .matchEffect (success , {onFailure : (error ) =>Micro .succeed (`failure: ${error .message }`).pipe (Micro .tap (consoleLog )),onSuccess : (value ) =>Micro .succeed (`success: ${value }`).pipe (Micro .tap (consoleLog ))})Micro .runSync (program1 )/*Output:success: 42*/constprogram2 =Micro .matchEffect (failure , {onFailure : (error ) =>Micro .succeed (`failure: ${error .message }`).pipe (Micro .tap (consoleLog )),onSuccess : (value ) =>Micro .succeed (`success: ${value }`).pipe (Micro .tap (consoleLog ))})Micro .runSync (program2 )/*Output:failure: Uh oh!*/
ts
import * asMicro from "effect/Micro"constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))constsuccess :Micro .Micro <number,Error > =Micro .succeed (42)constfailure :Micro .Micro <number,Error > =Micro .fail (newError ("Uh oh!"))constprogram1 =Micro .matchEffect (success , {onFailure : (error ) =>Micro .succeed (`failure: ${error .message }`).pipe (Micro .tap (consoleLog )),onSuccess : (value ) =>Micro .succeed (`success: ${value }`).pipe (Micro .tap (consoleLog ))})Micro .runSync (program1 )/*Output:success: 42*/constprogram2 =Micro .matchEffect (failure , {onFailure : (error ) =>Micro .succeed (`failure: ${error .message }`).pipe (Micro .tap (consoleLog )),onSuccess : (value ) =>Micro .succeed (`success: ${value }`).pipe (Micro .tap (consoleLog ))})Micro .runSync (program2 )/*Output:failure: Uh oh!*/
matchCause / matchCauseEffect
ts
import * asMicro from "effect/Micro"declare constexceptionalEffect :Micro .Micro <void,Error >constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))constprogram =Micro .matchCauseEffect (exceptionalEffect , {onFailure : (cause ) => {switch (cause ._tag ) {case "Fail":returnconsoleLog (`Fail: ${cause .error .message }`)case "Die":returnconsoleLog (`Die: ${cause .defect }`)case "Interrupt":returnconsoleLog ("interrupted!")}},onSuccess : (value ) =>consoleLog (`succeeded with ${value } value`)})
ts
import * asMicro from "effect/Micro"declare constexceptionalEffect :Micro .Micro <void,Error >constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))constprogram =Micro .matchCauseEffect (exceptionalEffect , {onFailure : (cause ) => {switch (cause ._tag ) {case "Fail":returnconsoleLog (`Fail: ${cause .error .message }`)case "Die":returnconsoleLog (`Die: ${cause .defect }`)case "Interrupt":returnconsoleLog ("interrupted!")}},onSuccess : (value ) =>consoleLog (`succeeded with ${value } value`)})
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:
ts
import * asMicro from "effect/Micro"letcount = 0// Simulates an effect with possible failuresexport consteffect =Micro .async <string,Error >((resume ) => {if (count <= 2) {count ++console .log ("failure")resume (Micro .fail (newError ()))} else {console .log ("success")resume (Micro .succeed ("yay!"))}})
ts
import * asMicro from "effect/Micro"letcount = 0// Simulates an effect with possible failuresexport consteffect =Micro .async <string,Error >((resume ) => {if (count <= 2) {count ++console .log ("failure")resume (Micro .fail (newError ()))} else {console .log ("success")resume (Micro .succeed ("yay!"))}})
retry
ts
import * asMicro from "effect/Micro"import {effect } from "./simulated-effect"// Define a repetition policy using a spaced delay between retriesconstpolicy =Micro .scheduleSpaced (100)constrepeated =Micro .retry (effect , {schedule :policy })Micro .runPromise (repeated ).then (console .log )/*Output:failurefailurefailuresuccessyay!*/
ts
import * asMicro from "effect/Micro"import {effect } from "./simulated-effect"// Define a repetition policy using a spaced delay between retriesconstpolicy =Micro .scheduleSpaced (100)constrepeated =Micro .retry (effect , {schedule :policy })Micro .runPromise (repeated ).then (console .log )/*Output:failurefailurefailuresuccessyay!*/
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.
ts
import * asMicro from "effect/Micro"constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))consteffect =Micro .fail ("Oh uh!").pipe (Micro .as ("primary result"))constsandboxed =Micro .sandbox (effect )constprogram =sandboxed .pipe (Micro .catchTag ("Fail", (cause ) =>consoleLog (`Caught a defect: ${cause .error }`).pipe (Micro .as ("fallback result on expected error"))),Micro .catchTag ("Interrupt", () =>consoleLog (`Caught a defect`).pipe (Micro .as ("fallback result on fiber interruption"))),Micro .catchTag ("Die", (cause ) =>consoleLog (`Caught a defect: ${cause .defect }`).pipe (Micro .as ("fallback result on unexpected error"))))Micro .runPromise (program ).then (console .log )/*Output:Caught a defect: Oh uh!fallback result on expected error*/
ts
import * asMicro from "effect/Micro"constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))consteffect =Micro .fail ("Oh uh!").pipe (Micro .as ("primary result"))constsandboxed =Micro .sandbox (effect )constprogram =sandboxed .pipe (Micro .catchTag ("Fail", (cause ) =>consoleLog (`Caught a defect: ${cause .error }`).pipe (Micro .as ("fallback result on expected error"))),Micro .catchTag ("Interrupt", () =>consoleLog (`Caught a defect`).pipe (Micro .as ("fallback result on fiber interruption"))),Micro .catchTag ("Die", (cause ) =>consoleLog (`Caught a defect: ${cause .defect }`).pipe (Micro .as ("fallback result on unexpected error"))))Micro .runPromise (program ).then (console .log )/*Output:Caught a defect: Oh uh!fallback result on expected error*/
Inspecting Errors
tapError
Executes an effectful operation to inspect the failure of an effect without altering it.
ts
import * asMicro from "effect/Micro"constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))// Create an effect that is designed to fail, simulating an occurrence of a network errorconsttask :Micro .Micro <number, string> =Micro .fail ("NetworkError")// Log the error message if the task fails. This function only executes if there is an error,// providing a method to handle or inspect errors without altering the outcome of the original effect.consttapping =Micro .tapError (task , (error ) =>consoleLog (`expected error: ${error }`))Micro .runFork (tapping )/*Output:expected error: NetworkError*/
ts
import * asMicro from "effect/Micro"constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))// Create an effect that is designed to fail, simulating an occurrence of a network errorconsttask :Micro .Micro <number, string> =Micro .fail ("NetworkError")// Log the error message if the task fails. This function only executes if there is an error,// providing a method to handle or inspect errors without altering the outcome of the original effect.consttapping =Micro .tapError (task , (error ) =>consoleLog (`expected error: ${error }`))Micro .runFork (tapping )/*Output:expected error: NetworkError*/
tapErrorCause
Inspects the underlying cause of an effect's failure.
ts
import * asMicro from "effect/Micro"constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))// Create an effect that is designed to fail, simulating an occurrence of a network errorconsttask1 :Micro .Micro <number, string> =Micro .fail ("NetworkError")// This will log the cause of any expected error or defectconsttapping1 =Micro .tapErrorCause (task1 , (cause ) =>consoleLog (`error cause: ${cause }`))Micro .runFork (tapping1 )/*Output:error cause: MicroCause.Fail: NetworkError*/// Simulate a severe failure in the system by causing a defect with a specific message.consttask2 :Micro .Micro <number, string> =Micro .die ("Something went wrong")// This will log the cause of any expected error or defectconsttapping2 =Micro .tapErrorCause (task2 , (cause ) =>consoleLog (`error cause: ${cause }`))Micro .runFork (tapping2 )/*Output:error cause: MicroCause.Die: Something went wrong*/
ts
import * asMicro from "effect/Micro"constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))// Create an effect that is designed to fail, simulating an occurrence of a network errorconsttask1 :Micro .Micro <number, string> =Micro .fail ("NetworkError")// This will log the cause of any expected error or defectconsttapping1 =Micro .tapErrorCause (task1 , (cause ) =>consoleLog (`error cause: ${cause }`))Micro .runFork (tapping1 )/*Output:error cause: MicroCause.Fail: NetworkError*/// Simulate a severe failure in the system by causing a defect with a specific message.consttask2 :Micro .Micro <number, string> =Micro .die ("Something went wrong")// This will log the cause of any expected error or defectconsttapping2 =Micro .tapErrorCause (task2 , (cause ) =>consoleLog (`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.
ts
import * asMicro from "effect/Micro"constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))// Create an effect that is designed to fail, simulating an occurrence of a network errorconsttask1 :Micro .Micro <number, string> =Micro .fail ("NetworkError")// this won't log anything because is not a defectconsttapping1 =Micro .tapDefect (task1 , (cause ) =>consoleLog (`defect: ${cause }`))Micro .runFork (tapping1 )/*No Output*/// Simulate a severe failure in the system by causing a defect with a specific message.consttask2 :Micro .Micro <number, string> =Micro .die ("Something went wrong")// This will only log defects, not errorsconsttapping2 =Micro .tapDefect (task2 , (cause ) =>consoleLog (`defect: ${cause }`))Micro .runFork (tapping2 )/*Output:defect: Something went wrong*/
ts
import * asMicro from "effect/Micro"constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))// Create an effect that is designed to fail, simulating an occurrence of a network errorconsttask1 :Micro .Micro <number, string> =Micro .fail ("NetworkError")// this won't log anything because is not a defectconsttapping1 =Micro .tapDefect (task1 , (cause ) =>consoleLog (`defect: ${cause }`))Micro .runFork (tapping1 )/*No Output*/// Simulate a severe failure in the system by causing a defect with a specific message.consttask2 :Micro .Micro <number, string> =Micro .die ("Something went wrong")// This will only log defects, not errorsconsttapping2 =Micro .tapDefect (task2 , (cause ) =>consoleLog (`defect: ${cause }`))Micro .runFork (tapping2 )/*Output:defect: Something went wrong*/
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
ts
import * asMicro from "effect/Micro"classMyError extendsMicro .Error <{message : string }> {}export constprogram =Micro .gen (function* () {// same as yield* Effect.fail(new MyError({ message: "Oh no!" })yield* newMyError ({message : "Oh no!" })})Micro .runPromiseExit (program ).then (console .log )/*Output:{_id: 'Either',_tag: 'Left',left: (MicroCause.Fail) Error: Oh no!...stack trace...}*/
ts
import * asMicro from "effect/Micro"classMyError extendsMicro .Error <{message : string }> {}export constprogram =Micro .gen (function* () {// same as yield* Effect.fail(new MyError({ message: "Oh no!" })yield* newMyError ({message : "Oh no!" })})Micro .runPromiseExit (program ).then (console .log )/*Output:{_id: 'Either',_tag: 'Left',left: (MicroCause.Fail) Error: Oh no!...stack trace...}*/
TaggedError
ts
import * asMicro from "effect/Micro"// An error with _tag: "Foo"classFooError extendsMicro .TaggedError ("Foo")<{message : string}> {}// An error with _tag: "Bar"classBarError extendsMicro .TaggedError ("Bar")<{randomNumber : number}> {}export constprogram =Micro .gen (function* () {constn =Math .random ()returnn > 0.5? "yay!":n < 0.2? yield* newFooError ({message : "Oh no!" }): yield* newBarError ({randomNumber :n })}).pipe (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!*/
ts
import * asMicro from "effect/Micro"// An error with _tag: "Foo"classFooError extendsMicro .TaggedError ("Foo")<{message : string}> {}// An error with _tag: "Bar"classBarError extendsMicro .TaggedError ("Bar")<{randomNumber : number}> {}export constprogram =Micro .gen (function* () {constn =Math .random ()returnn > 0.5? "yay!":n < 0.2? yield* newFooError ({message : "Oh no!" }): yield* newBarError ({randomNumber :n })}).pipe (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
import * asContext from "effect/Context"import * asMicro from "effect/Micro"// Define a service using a unique identifierclassRandom extendsContext .Tag ("MyRandomService")<Random ,{ readonlynext :Micro .Micro <number> } // Operations>() {}
ts
import * asContext from "effect/Context"import * asMicro from "effect/Micro"// Define a service using a unique identifierclassRandom extendsContext .Tag ("MyRandomService")<Random ,{ readonlynext :Micro .Micro <number> } // Operations>() {}
Now that we have our service tag defined, let's see how we can use it by building a simple program.
ts
constprogram =Micro .gen (function* () {// Access the Random serviceconstrandom = yield*Micro .service (Random )// Retrieve a random number from the serviceconstrandomNumber = yield*random .next console .log (`random number: ${randomNumber }`)})
ts
constprogram =Micro .gen (function* () {// Access the Random serviceconstrandom = yield*Micro .service (Random )// Retrieve a random number from the serviceconstrandomNumber = 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: 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.
ts
// Provide the Random service implementationconstrunnable =Micro .provideService (program ,Random , {next :Micro .sync (() =>Math .random ())})// Execute the program and print the random numberMicro .runPromise (runnable )/*Example Output:random number: 0.8241872233134417*/
ts
// Provide the Random service implementationconstrunnable =Micro .provideService (program ,Random , {next :Micro .sync (() =>Math .random ())})// Execute the program and print the random numberMicro .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, which represent the release of a resource.
- Close the scope, releasing all acquired resources and executing any added finalizers.
ts
import * asMicro from "effect/Micro"constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))constprogram =// create a new scopeMicro .scopeMake .pipe (// add finalizer 1Micro .tap ((scope ) =>scope .addFinalizer (() =>consoleLog ("finalizer 1"))),// add finalizer 2Micro .tap ((scope ) =>scope .addFinalizer (() =>consoleLog ("finalizer 2"))),// close the scopeMicro .andThen ((scope ) =>scope .close (Micro .exitSucceed ("scope closed successfully"))))Micro .runPromise (program )/*Output:finalizer 2 <-- finalizers are closed in reverse orderfinalizer 1*/
ts
import * asMicro from "effect/Micro"constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))constprogram =// create a new scopeMicro .scopeMake .pipe (// add finalizer 1Micro .tap ((scope ) =>scope .addFinalizer (() =>consoleLog ("finalizer 1"))),// add finalizer 2Micro .tap ((scope ) =>scope .addFinalizer (() =>consoleLog ("finalizer 2"))),// close the scopeMicro .andThen ((scope ) =>scope .close (Micro .exitSucceed ("scope closed successfully"))))Micro .runPromise (program )/*Output:finalizer 2 <-- finalizers are closed in reverse orderfinalizer 1*/
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:
ts
import * asMicro from "effect/Micro"constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))constprogram =Micro .gen (function* () {yield*Micro .addFinalizer ((exit ) =>consoleLog (`finalizer after ${exit ._tag }`))return 1})construnnable =Micro .scoped (program )Micro .runPromise (runnable ).then (console .log ,console .error )/*Output:finalizer after Right1*/
ts
import * asMicro from "effect/Micro"constconsoleLog = (message : string) =>Micro .sync (() =>console .log (message ))constprogram =Micro .gen (function* () {yield*Micro .addFinalizer ((exit ) =>consoleLog (`finalizer after ${exit ._tag }`))return 1})construnnable =Micro .scoped (program )Micro .runPromise (runnable ).then (console .log ,console .error )/*Output:finalizer after Right1*/
Next, let's explore how things behave in the event of a failure:
ts
import * asMicro from "effect/Micro"constconsoleLog = (message ?: any, ...optionalParams :Array <any>) =>Micro .sync (() =>console .log (message , ...optionalParams ))constprogram =Micro .gen (function* () {yield*Micro .addFinalizer ((exit ) =>consoleLog (`finalizer after ${exit ._tag }`))return yield*Micro .fail ("Uh oh!")})construnnable =Micro .scoped (program )Micro .runPromiseExit (runnable ).then (console .log )/*Output:finalizer after Left{ _id: 'Either', _tag: 'Left', left: MicroCause.Fail: Uh oh! }*/
ts
import * asMicro from "effect/Micro"constconsoleLog = (message ?: any, ...optionalParams :Array <any>) =>Micro .sync (() =>console .log (message , ...optionalParams ))constprogram =Micro .gen (function* () {yield*Micro .addFinalizer ((exit ) =>consoleLog (`finalizer after ${exit ._tag }`))return yield*Micro .fail ("Uh oh!")})construnnable =Micro .scoped (program )Micro .runPromiseExit (runnable ).then (console .log )/*Output:finalizer after Left{ _id: 'Either', _tag: 'Left', left: MicroCause.Fail: 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.
For example, let's define a simple resource:
ts
import * asMicro from "effect/Micro"// Define the interface for the resourceinterfaceMyResource {readonlycontents : stringreadonlyclose : () =>Promise <void>}// Simulate getting the resourceconstgetMyResource = ():Promise <MyResource > =>Promise .resolve ({contents : "lorem ipsum",close : () =>newPromise ((resolve ) => {console .log ("Resource released")resolve ()})})// Define the acquisition of the resource with error handlingconstacquire =Micro .tryPromise ({try : () =>getMyResource ().then ((res ) => {console .log ("Resource acquired")returnres }),catch : () => newError ("getMyResourceError")})// Define the release of the resourceconstrelease = (res :MyResource ) =>Micro .promise (() =>res .close ())constresource =Micro .acquireRelease (acquire ,release )constprogram =Micro .scoped (Micro .gen (function* () {constres = yield*resource console .log (`content is ${res .contents }`)}))Micro .runPromise (program )/*Resource acquiredcontent is lorem ipsumResource released*/
ts
import * asMicro from "effect/Micro"// Define the interface for the resourceinterfaceMyResource {readonlycontents : stringreadonlyclose : () =>Promise <void>}// Simulate getting the resourceconstgetMyResource = ():Promise <MyResource > =>Promise .resolve ({contents : "lorem ipsum",close : () =>newPromise ((resolve ) => {console .log ("Resource released")resolve ()})})// Define the acquisition of the resource with error handlingconstacquire =Micro .tryPromise ({try : () =>getMyResource ().then ((res ) => {console .log ("Resource acquired")returnres }),catch : () => newError ("getMyResourceError")})// Define the release of the resourceconstrelease = (res :MyResource ) =>Micro .promise (() =>res .close ())constresource =Micro .acquireRelease (acquire ,release )constprogram =Micro .scoped (Micro .gen (function* () {constres = yield*resource console .log (`content is ${res .contents }`)}))Micro .runPromise (program )/*Resource acquiredcontent is lorem ipsumResource 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.
Here's an example that demonstrates the usage of acquireUseRelease
:
ts
import * asMicro from "effect/Micro"// Define the interface for the resourceinterfaceMyResource {readonlycontents : stringreadonlyclose : () =>Promise <void>}// Simulate getting the resourceconstgetMyResource = ():Promise <MyResource > =>Promise .resolve ({contents : "lorem ipsum",close : () =>newPromise ((resolve ) => {console .log ("Resource released")resolve ()})})// Define the acquisition of the resource with error handlingconstacquire =Micro .tryPromise ({try : () =>getMyResource ().then ((res ) => {console .log ("Resource acquired")returnres }),catch : () => newError ("getMyResourceError")})// Define the release of the resourceconstrelease = (res :MyResource ) =>Micro .promise (() =>res .close ())constuse = (res :MyResource ) =>Micro .sync (() =>console .log (`content is ${res .contents }`))constprogram =Micro .acquireUseRelease (acquire ,use ,release )Micro .runPromise (program )/*Resource acquiredcontent is lorem ipsumResource released*/
ts
import * asMicro from "effect/Micro"// Define the interface for the resourceinterfaceMyResource {readonlycontents : stringreadonlyclose : () =>Promise <void>}// Simulate getting the resourceconstgetMyResource = ():Promise <MyResource > =>Promise .resolve ({contents : "lorem ipsum",close : () =>newPromise ((resolve ) => {console .log ("Resource released")resolve ()})})// Define the acquisition of the resource with error handlingconstacquire =Micro .tryPromise ({try : () =>getMyResource ().then ((res ) => {console .log ("Resource acquired")returnres }),catch : () => newError ("getMyResourceError")})// Define the release of the resourceconstrelease = (res :MyResource ) =>Micro .promise (() =>res .close ())constuse = (res :MyResource ) =>Micro .sync (() =>console .log (`content is ${res .contents }`))constprogram =Micro .acquireUseRelease (acquire ,use ,release )Micro .runPromise (program )/*Resource acquiredcontent is lorem ipsumResource released*/
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.
The scheduled recurrences are in addition to the initial execution, so
Effect.repeat(action, Micro.scheduleRecurs(1))
executes action
once
initially, and if it succeeds, repeats it an additional time.
Success Example
ts
import * asMicro from "effect/Micro"constaction =Micro .sync (() =>console .log ("success"))constpolicy =Micro .scheduleAddDelay (Micro .scheduleRecurs (2), () => 100)constprogram =Micro .repeat (action , {schedule :policy })Micro .runPromise (program ).then ((n ) =>console .log (`repetitions: ${n }`))/*Output:successsuccesssuccess*/
ts
import * asMicro from "effect/Micro"constaction =Micro .sync (() =>console .log ("success"))constpolicy =Micro .scheduleAddDelay (Micro .scheduleRecurs (2), () => 100)constprogram =Micro .repeat (action , {schedule :policy })Micro .runPromise (program ).then ((n ) =>console .log (`repetitions: ${n }`))/*Output:successsuccesssuccess*/
Failure Example
ts
import * asMicro from "effect/Micro"letcount = 0// Define an async effect that simulates an action with possible failuresconstaction =Micro .async <string, string>((resume ) => {if (count > 1) {console .log ("failure")resume (Micro .fail ("Uh oh!"))} else {count ++console .log ("success")resume (Micro .succeed ("yay!"))}})constpolicy =Micro .scheduleAddDelay (Micro .scheduleRecurs (2), () => 100)constprogram =Micro .repeat (action , {schedule :policy })Micro .runPromiseExit (program ).then (console .log )/*Output:successsuccessfailure{ _id: 'Either', _tag: 'Left', left: MicroCause.Fail: Uh oh! }*/
ts
import * asMicro from "effect/Micro"letcount = 0// Define an async effect that simulates an action with possible failuresconstaction =Micro .async <string, string>((resume ) => {if (count > 1) {console .log ("failure")resume (Micro .fail ("Uh oh!"))} else {count ++console .log ("success")resume (Micro .succeed ("yay!"))}})constpolicy =Micro .scheduleAddDelay (Micro .scheduleRecurs (2), () => 100)constprogram =Micro .repeat (action , {schedule :policy })Micro .runPromiseExit (program ).then (console .log )/*Output:successsuccessfailure{ _id: 'Either', _tag: 'Left', left: MicroCause.Fail: Uh oh! }*/
helper
To demonstrate the functionality of different schedules, we will be working with the following helper:
ts
import type * asMicro from "effect/Micro"import * asOption from "effect/Option"export constdryRun = (schedule :Micro .MicroSchedule ,maxAttempt : number = 7):Array <number> => {letattempt = 1letelapsed = 0letduration =schedule (attempt ,elapsed )constout :Array <number> = []while (Option .isSome (duration ) &&attempt <=maxAttempt ) {constvalue =duration .value attempt ++elapsed +=value out .push (value )duration =schedule (attempt ,elapsed )}returnout }
ts
import type * asMicro from "effect/Micro"import * asOption from "effect/Option"export constdryRun = (schedule :Micro .MicroSchedule ,maxAttempt : number = 7):Array <number> => {letattempt = 1letelapsed = 0letduration =schedule (attempt ,elapsed )constout :Array <number> = []while (Option .isSome (duration ) &&attempt <=maxAttempt ) {constvalue =duration .value attempt ++elapsed +=value out .push (value )duration =schedule (attempt ,elapsed )}returnout }
scheduleSpaced
A schedule that recurs continuously, each repetition spaced the specified duration from the last run.
ts
import * asMicro from "effect/Micro"import {dryRun } from "./dryRun"constpolicy =Micro .scheduleSpaced (10)console .log (dryRun (policy ))/*Output:[10, 10, 10, 10,10, 10, 10]*/
ts
import * asMicro from "effect/Micro"import {dryRun } from "./dryRun"constpolicy =Micro .scheduleSpaced (10)console .log (dryRun (policy ))/*Output:[10, 10, 10, 10,10, 10, 10]*/
scheduleExponential
A schedule that recurs using exponential backoff.
ts
import * asMicro from "effect/Micro"import {dryRun } from "./dryRun"constpolicy =Micro .scheduleExponential (10)console .log (dryRun (policy ))/*Output:[20, 40, 80,160, 320, 640,1280]*/
ts
import * asMicro from "effect/Micro"import {dryRun } from "./dryRun"constpolicy =Micro .scheduleExponential (10)console .log (dryRun (policy ))/*Output:[20, 40, 80,160, 320, 640,1280]*/
scheduleUnion
Combines two schedules through union, by recurring if either schedule wants to recur, using the minimum of the two delays between recurrences.
ts
import * asMicro from "effect/Micro"import {dryRun } from "./dryRun"constpolicy =Micro .scheduleUnion (Micro .scheduleExponential (10),Micro .scheduleSpaced (300))console .log (dryRun (policy ))/*Output:[20, < exponential40,80,160,300, < spaced300,300]*/
ts
import * asMicro from "effect/Micro"import {dryRun } from "./dryRun"constpolicy =Micro .scheduleUnion (Micro .scheduleExponential (10),Micro .scheduleSpaced (300))console .log (dryRun (policy ))/*Output:[20, < exponential40,80,160,300, < spaced300,300]*/
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.
ts
import * asMicro from "effect/Micro"import {dryRun } from "./dryRun"constpolicy =Micro .scheduleIntersect (Micro .scheduleExponential (10),Micro .scheduleSpaced (300))console .log (dryRun (policy ))/*Output:[300, < spaced300,300,300,320, < exponential640,1280]*/
ts
import * asMicro from "effect/Micro"import {dryRun } from "./dryRun"constpolicy =Micro .scheduleIntersect (Micro .scheduleExponential (10),Micro .scheduleSpaced (300))console .log (dryRun (policy ))/*Output:[300, < spaced300,300,300,320, < exponential640,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:
ts
import * asMicro from "effect/Micro"constfib = (n : number):Micro .Micro <number> =>Micro .suspend (() => {if (n <= 1) {returnMicro .succeed (n )}returnfib (n - 1).pipe (Micro .zipWith (fib (n - 2), (a ,b ) =>a +b ))})constfib10Fiber =Micro .fork (fib (10))
ts
import * asMicro from "effect/Micro"constfib = (n : number):Micro .Micro <number> =>Micro .suspend (() => {if (n <= 1) {returnMicro .succeed (n )}returnfib (n - 1).pipe (Micro .zipWith (fib (n - 2), (a ,b ) =>a +b ))})constfib10Fiber =Micro .fork (fib (10))
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:
ts
import * asMicro from "effect/Micro"constfib = (n : number):Micro .Micro <number> =>Micro .suspend (() => {if (n <= 1) {returnMicro .succeed (n )}returnfib (n - 1).pipe (Micro .zipWith (fib (n - 2), (a ,b ) =>a +b ))})constfib10Fiber =Micro .fork (fib (10))constprogram =Micro .gen (function* () {constfiber = yield*fib10Fiber constn = yield*fiber .join console .log (n )})Micro .runPromise (program ) // 55
ts
import * asMicro from "effect/Micro"constfib = (n : number):Micro .Micro <number> =>Micro .suspend (() => {if (n <= 1) {returnMicro .succeed (n )}returnfib (n - 1).pipe (Micro .zipWith (fib (n - 2), (a ,b ) =>a +b ))})constfib10Fiber =Micro .fork (fib (10))constprogram =Micro .gen (function* () {constfiber = yield*fib10Fiber constn = yield*fiber .join console .log (n )})Micro .runPromise (program ) // 55
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.
ts
import * asMicro from "effect/Micro"constfib = (n : number):Micro .Micro <number> =>Micro .suspend (() => {if (n <= 1) {returnMicro .succeed (n )}returnfib (n - 1).pipe (Micro .zipWith (fib (n - 2), (a ,b ) =>a +b ))})constfib10Fiber =Micro .fork (fib (10))constprogram =Micro .gen (function* () {constfiber = yield*fib10Fiber constexit = yield*fiber .await console .log (exit )})Micro .runPromise (program ) // { _id: 'Either', _tag: 'Right', right: 55 }
ts
import * asMicro from "effect/Micro"constfib = (n : number):Micro .Micro <number> =>Micro .suspend (() => {if (n <= 1) {returnMicro .succeed (n )}returnfib (n - 1).pipe (Micro .zipWith (fib (n - 2), (a ,b ) =>a +b ))})constfib10Fiber =Micro .fork (fib (10))constprogram =Micro .gen (function* () {constfiber = yield*fib10Fiber constexit = yield*fiber .await console .log (exit )})Micro .runPromise (program ) // { _id: 'Either', _tag: 'Right', right: 55 }
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.
ts
import * asMicro from "effect/Micro"constprogram =Micro .gen (function* () {constfiber = yield*Micro .fork (Micro .forever (Micro .succeed ("Hi!")))constexit = yield*fiber .interrupt console .log (exit )})Micro .runPromise (program )/*Output{_id: 'Either',_tag: 'Left',left: MicroCause.Interrupt: interrupted}*/
ts
import * asMicro from "effect/Micro"constprogram =Micro .gen (function* () {constfiber = yield*Micro .fork (Micro .forever (Micro .succeed ("Hi!")))constexit = yield*fiber .interrupt console .log (exit )})Micro .runPromise (program )/*Output{_id: 'Either',_tag: 'Left',left: MicroCause.Interrupt: interrupted}*/
Racing
The Micro.race
function lets you race multiple effects concurrently and returns the result of the first one that successfully completes.
ts
import * asMicro from "effect/Micro"consttask1 =Micro .delay (Micro .fail ("task1"), 1_000)consttask2 =Micro .delay (Micro .succeed ("task2"), 2_000)constprogram =Micro .race (task1 ,task2 )Micro .runPromise (program ).then (console .log )/*Output:task2*/
ts
import * asMicro from "effect/Micro"consttask1 =Micro .delay (Micro .fail ("task1"), 1_000)consttask2 =Micro .delay (Micro .succeed ("task2"), 2_000)constprogram =Micro .race (task1 ,task2 )Micro .runPromise (program ).then (console .log )/*Output:task2*/
If you need to handle the first effect to complete, whether it succeeds or fails, you can use the Micro.either
function.
ts
import * asMicro from "effect/Micro"consttask1 =Micro .delay (Micro .fail ("task1"), 1_000)consttask2 =Micro .delay (Micro .succeed ("task2"), 2_000)constprogram =Micro .race (Micro .either (task1 ),Micro .either (task2 ))Micro .runPromise (program ).then (console .log )/*Output:{ _id: 'Either', _tag: 'Left', left: 'task1' }*/
ts
import * asMicro from "effect/Micro"consttask1 =Micro .delay (Micro .fail ("task1"), 1_000)consttask2 =Micro .delay (Micro .succeed ("task2"), 2_000)constprogram =Micro .race (Micro .either (task1 ),Micro .either (task2 ))Micro .runPromise (program ).then (console .log )/*Output:{ _id: 'Either', _tag: 'Left', left: 'task1' }*/
Timing out
Interruptible Operation: If the operation can be interrupted, it is terminated immediately once the timeout threshold is reached, resulting in a TimeoutException
.
ts
import * asMicro from "effect/Micro"constmyEffect =Micro .gen (function* () {console .log ("Start processing...")yield*Micro .sleep (2_000) // Simulates a delay in processingconsole .log ("Processing complete.")return "Result"})consttimedEffect =myEffect .pipe (Micro .timeout (1_000))Micro .runPromiseExit (timedEffect ).then (console .log )/*Output:{_id: 'Either',_tag: 'Left',left: (MicroCause.Fail) TimeoutException...stack trace...}*/
ts
import * asMicro from "effect/Micro"constmyEffect =Micro .gen (function* () {console .log ("Start processing...")yield*Micro .sleep (2_000) // Simulates a delay in processingconsole .log ("Processing complete.")return "Result"})consttimedEffect =myEffect .pipe (Micro .timeout (1_000))Micro .runPromiseExit (timedEffect ).then (console .log )/*Output:{_id: 'Either',_tag: 'Left',left: (MicroCause.Fail) TimeoutException...stack trace...}*/
Uninterruptible Operation: If the operation is uninterruptible, it continues until completion before the TimeoutException
is assessed.
ts
import * asMicro from "effect/Micro"constmyEffect =Micro .gen (function* () {console .log ("Start processing...")yield*Micro .sleep (2_000) // Simulates a delay in processingconsole .log ("Processing complete.")return "Result"})consttimedEffect =myEffect .pipe (Micro .uninterruptible ,Micro .timeout (1_000))// Outputs a TimeoutException after the task completes, because the task is uninterruptibleMicro .runPromiseExit (timedEffect ).then (console .log )/*Output:Start processing...Processing complete.{_id: 'Either',_tag: 'Left',left: (MicroCause.Fail) TimeoutException...stack trace...}*/
ts
import * asMicro from "effect/Micro"constmyEffect =Micro .gen (function* () {console .log ("Start processing...")yield*Micro .sleep (2_000) // Simulates a delay in processingconsole .log ("Processing complete.")return "Result"})consttimedEffect =myEffect .pipe (Micro .uninterruptible ,Micro .timeout (1_000))// Outputs a TimeoutException after the task completes, because the task is uninterruptibleMicro .runPromiseExit (timedEffect ).then (console .log )/*Output:Start processing...Processing complete.{_id: 'Either',_tag: 'Left',left: (MicroCause.Fail) TimeoutException...stack trace...}*/
Calling Effect.interrupt
ts
import * asMicro from "effect/Micro"constprogram =Micro .gen (function* () {console .log ("waiting 1 second")yield*Micro .sleep (1_000)yield*Micro .interrupt console .log ("waiting 1 second")yield*Micro .sleep (1_000)console .log ("done")})Micro .runPromiseExit (program ).then (console .log )/*Output:waiting 1 second{_id: 'Either',_tag: 'Left',left: MicroCause.Interrupt: interrupted}*/
ts
import * asMicro from "effect/Micro"constprogram =Micro .gen (function* () {console .log ("waiting 1 second")yield*Micro .sleep (1_000)yield*Micro .interrupt console .log ("waiting 1 second")yield*Micro .sleep (1_000)console .log ("done")})Micro .runPromiseExit (program ).then (console .log )/*Output:waiting 1 second{_id: 'Either',_tag: 'Left',left: MicroCause.Interrupt: interrupted}*/
Interruption of Concurrent Effects
ts
import * asMicro from "effect/Micro"constprogram =Micro .forEach ([1, 2, 3],(n ) =>Micro .gen (function* () {console .log (`start #${n }`)yield*Micro .sleep (n * 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 #1start #2start #3done #1{"_id": "Either","_tag": "Left","left": {"_tag": "Interrupt","traces": [],"name": "MicroCause.Interrupt"}}*/
ts
import * asMicro from "effect/Micro"constprogram =Micro .forEach ([1, 2, 3],(n ) =>Micro .gen (function* () {console .log (`start #${n }`)yield*Micro .sleep (n * 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 #1start #2start #3done #1{"_id": "Either","_tag": "Left","left": {"_tag": "Interrupt","traces": [],"name": "MicroCause.Interrupt"}}*/