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 * as Micro from "effect/Micro"
ts
import * as Micro 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 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:

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 TypeDescription
DieIndicates 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.
InterruptSignifies 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
function fetchWeather(city: string): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (city === "London") {
resolve("Sunny")
} else {
reject(new Error("Weather data not found for this location"))
}
}, 1_000)
})
}
ts
function fetchWeather(city: string): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (city === "London") {
resolve("Sunny")
} else {
reject(new Error("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
function getWeather(city: string) {
return Micro.promise(() => fetchWeather(city))
}
ts
function getWeather(city: string) {
return Micro.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
const weatherEffect = 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
const weatherEffect = 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
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))
})
}
 
const weatherEffect = 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
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))
})
}
 
const weatherEffect = 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 * as Either from "effect/Either"
import * as Micro from "effect/Micro"
 
class NetworkError {
readonly _tag = "NetworkError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
 
const task = Micro.gen(function* () {
// Simulate network and validation errors
if (Math.random() > 0.5) yield* Micro.fail(new NetworkError())
if (Math.random() > 0.5) yield* Micro.fail(new ValidationError())
return "Success"
})
 
const recovered = Micro.gen(function* () {
const failureOrSuccess = yield* Micro.either(task)
return Either.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 * as Either from "effect/Either"
import * as Micro from "effect/Micro"
 
class NetworkError {
readonly _tag = "NetworkError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
 
const task = Micro.gen(function* () {
// Simulate network and validation errors
if (Math.random() > 0.5) yield* Micro.fail(new NetworkError())
if (Math.random() > 0.5) yield* Micro.fail(new ValidationError())
return "Success"
})
 
const recovered = Micro.gen(function* () {
const failureOrSuccess = yield* Micro.either(task)
return Either.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 * as Micro from "effect/Micro"
 
class NetworkError {
readonly _tag = "NetworkError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
 
const task = Micro.gen(function* () {
// Simulate network and validation errors
if (Math.random() > 0.5) yield* Micro.fail(new NetworkError())
if (Math.random() > 0.5) yield* Micro.fail(new ValidationError())
return "Success"
})
 
const recovered = 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 * as Micro from "effect/Micro"
 
class NetworkError {
readonly _tag = "NetworkError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
 
const task = Micro.gen(function* () {
// Simulate network and validation errors
if (Math.random() > 0.5) yield* Micro.fail(new NetworkError())
if (Math.random() > 0.5) yield* Micro.fail(new ValidationError())
return "Success"
})
 
const recovered = 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 * as Micro from "effect/Micro"
 
class NetworkError {
readonly _tag = "NetworkError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
 
const task = Micro.gen(function* () {
// Simulate network and validation errors
if (Math.random() > 0.5) yield* Micro.fail(new NetworkError())
if (Math.random() > 0.5) yield* Micro.fail(new ValidationError())
return "Success"
})
 
const recovered = 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 * as Micro from "effect/Micro"
 
class NetworkError {
readonly _tag = "NetworkError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
 
const task = Micro.gen(function* () {
// Simulate network and validation errors
if (Math.random() > 0.5) yield* Micro.fail(new NetworkError())
if (Math.random() > 0.5) yield* Micro.fail(new ValidationError())
return "Success"
})
 
const recovered = 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 * as Micro from "effect/Micro"
 
const divide = (a: number, b: number): Micro.Micro<number> =>
b === 0
? Micro.die(new Error("Cannot divide by zero"))
: Micro.succeed(a / b)
 
Micro.runSync(divide(1, 0)) // throws Error: Cannot divide by zero
ts
import * as Micro from "effect/Micro"
 
const divide = (a: number, b: number): Micro.Micro<number> =>
b === 0
? Micro.die(new Error("Cannot divide by zero"))
: Micro.succeed(a / b)
 
Micro.runSync(divide(1, 0)) // throws Error: Cannot divide by zero

orDie

ts
import * as Micro from "effect/Micro"
 
const divide = (a: number, b: number): Micro.Micro<number, Error> =>
b === 0
? Micro.fail(new Error("Cannot divide by zero"))
: Micro.succeed(a / b)
 
const program = Micro.orDie(divide(1, 0))
 
Micro.runSync(program) // throws Error: Cannot divide by zero
ts
import * as Micro from "effect/Micro"
 
const divide = (a: number, b: number): Micro.Micro<number, Error> =>
b === 0
? Micro.fail(new Error("Cannot divide by zero"))
: Micro.succeed(a / b)
 
const program = Micro.orDie(divide(1, 0))
 
Micro.runSync(program) // throws Error: Cannot divide by zero

catchAllDefect

ts
import * as Micro from "effect/Micro"
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
const program = Micro.catchAllDefect(
Micro.die("Boom!"), // Simulating a runtime error
(defect) => consoleLog(`Unknown defect caught: ${defect}`)
)
 
// We get an Either.Right because we caught all defects
Micro.runPromiseExit(program).then(console.log)
/*
Output:
Unknown defect caught: Boom!
{ _id: 'Either', _tag: 'Right', right: undefined }
*/
ts
import * as Micro from "effect/Micro"
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
const program = Micro.catchAllDefect(
Micro.die("Boom!"), // Simulating a runtime error
(defect) => consoleLog(`Unknown defect caught: ${defect}`)
)
 
// We get an Either.Right because we caught all defects
Micro.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 * as Micro from "effect/Micro"
 
class NegativeAgeError {
readonly _tag = "NegativeAgeError"
constructor(readonly age: number) {}
}
 
class IllegalAgeError {
readonly _tag = "IllegalAgeError"
constructor(readonly age: number) {}
}
 
const validate = (
age: number
): Micro.Micro<number, NegativeAgeError | IllegalAgeError> => {
if (age < 0) {
return Micro.fail(new NegativeAgeError(age))
} else if (age < 18) {
return Micro.fail(new IllegalAgeError(age))
} else {
return Micro.succeed(age)
}
}
 
const program = Micro.orElseSucceed(validate(3), () => 0)
ts
import * as Micro from "effect/Micro"
 
class NegativeAgeError {
readonly _tag = "NegativeAgeError"
constructor(readonly age: number) {}
}
 
class IllegalAgeError {
readonly _tag = "IllegalAgeError"
constructor(readonly age: number) {}
}
 
const validate = (
age: number
): Micro.Micro<number, NegativeAgeError | IllegalAgeError> => {
if (age < 0) {
return Micro.fail(new NegativeAgeError(age))
} else if (age < 18) {
return Micro.fail(new IllegalAgeError(age))
} else {
return Micro.succeed(age)
}
}
 
const program = Micro.orElseSucceed(validate(3), () => 0)

Matching

match

ts
import * as Micro from "effect/Micro"
 
const success: Micro.Micro<number, Error> = Micro.succeed(42)
const failure: Micro.Micro<number, Error> = Micro.fail(new Error("Uh oh!"))
 
const program1 = Micro.match(success, {
onFailure: (error) => `failure: ${error.message}`,
onSuccess: (value) => `success: ${value}`
})
 
Micro.runPromise(program1).then(console.log) // Output: "success: 42"
 
const program2 = Micro.match(failure, {
onFailure: (error) => `failure: ${error.message}`,
onSuccess: (value) => `success: ${value}`
})
 
Micro.runPromise(program2).then(console.log) // Output: "failure: Uh oh!"
ts
import * as Micro from "effect/Micro"
 
const success: Micro.Micro<number, Error> = Micro.succeed(42)
const failure: Micro.Micro<number, Error> = Micro.fail(new Error("Uh oh!"))
 
const program1 = Micro.match(success, {
onFailure: (error) => `failure: ${error.message}`,
onSuccess: (value) => `success: ${value}`
})
 
Micro.runPromise(program1).then(console.log) // Output: "success: 42"
 
const program2 = 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 * as Micro from "effect/Micro"
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
const success: Micro.Micro<number, Error> = Micro.succeed(42)
const failure: Micro.Micro<number, Error> = Micro.fail(new Error("Uh oh!"))
 
const program1 = 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
*/
 
const program2 = 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 * as Micro from "effect/Micro"
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
const success: Micro.Micro<number, Error> = Micro.succeed(42)
const failure: Micro.Micro<number, Error> = Micro.fail(new Error("Uh oh!"))
 
const program1 = 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
*/
 
const program2 = 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 * as Micro from "effect/Micro"
 
declare const exceptionalEffect: Micro.Micro<void, Error>
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
const program = Micro.matchCauseEffect(exceptionalEffect, {
onFailure: (cause) => {
switch (cause._tag) {
case "Fail":
return consoleLog(`Fail: ${cause.error.message}`)
case "Die":
return consoleLog(`Die: ${cause.defect}`)
case "Interrupt":
return consoleLog("interrupted!")
}
},
onSuccess: (value) => consoleLog(`succeeded with ${value} value`)
})
ts
import * as Micro from "effect/Micro"
 
declare const exceptionalEffect: Micro.Micro<void, Error>
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
const program = Micro.matchCauseEffect(exceptionalEffect, {
onFailure: (cause) => {
switch (cause._tag) {
case "Fail":
return consoleLog(`Fail: ${cause.error.message}`)
case "Die":
return consoleLog(`Die: ${cause.defect}`)
case "Interrupt":
return consoleLog("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 * as Micro from "effect/Micro"
 
let count = 0
 
// Simulates an effect with possible failures
export const effect = Micro.async<string, Error>((resume) => {
if (count <= 2) {
count++
console.log("failure")
resume(Micro.fail(new Error()))
} else {
console.log("success")
resume(Micro.succeed("yay!"))
}
})
ts
import * as Micro from "effect/Micro"
 
let count = 0
 
// Simulates an effect with possible failures
export const effect = Micro.async<string, Error>((resume) => {
if (count <= 2) {
count++
console.log("failure")
resume(Micro.fail(new Error()))
} else {
console.log("success")
resume(Micro.succeed("yay!"))
}
})

retry

ts
import * as Micro from "effect/Micro"
import { effect } from "./simulated-effect"
 
// 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!
*/
ts
import * as Micro from "effect/Micro"
import { effect } from "./simulated-effect"
 
// 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!
*/

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 * as Micro from "effect/Micro"
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
const effect = Micro.fail("Oh uh!").pipe(Micro.as("primary result"))
 
const sandboxed = Micro.sandbox(effect)
 
const program = 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 * as Micro from "effect/Micro"
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
const effect = Micro.fail("Oh uh!").pipe(Micro.as("primary result"))
 
const sandboxed = Micro.sandbox(effect)
 
const program = 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 * as Micro from "effect/Micro"
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
// Create an effect that is designed to fail, simulating an occurrence of a network error
const task: 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.
const tapping = Micro.tapError(task, (error) =>
consoleLog(`expected error: ${error}`)
)
 
Micro.runFork(tapping)
/*
Output:
expected error: NetworkError
*/
ts
import * as Micro from "effect/Micro"
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
// Create an effect that is designed to fail, simulating an occurrence of a network error
const task: 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.
const tapping = 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 * as Micro from "effect/Micro"
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
// Create an effect that is designed to fail, simulating an occurrence of a network error
const task1: Micro.Micro<number, string> = Micro.fail("NetworkError")
// This will log the cause of any expected error or defect
const tapping1 = 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.
const task2: Micro.Micro<number, string> = Micro.die("Something went wrong")
 
// This will log the cause of any expected error or defect
const tapping2 = Micro.tapErrorCause(task2, (cause) =>
consoleLog(`error cause: ${cause}`)
)
 
Micro.runFork(tapping2)
/*
Output:
error cause: MicroCause.Die: Something went wrong
*/
ts
import * as Micro from "effect/Micro"
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
// Create an effect that is designed to fail, simulating an occurrence of a network error
const task1: Micro.Micro<number, string> = Micro.fail("NetworkError")
// This will log the cause of any expected error or defect
const tapping1 = 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.
const task2: Micro.Micro<number, string> = Micro.die("Something went wrong")
 
// This will log the cause of any expected error or defect
const tapping2 = 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 * as Micro from "effect/Micro"
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
// Create an effect that is designed to fail, simulating an occurrence of a network error
const task1: Micro.Micro<number, string> = Micro.fail("NetworkError")
 
// this won't log anything because is not a defect
const tapping1 = 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.
const task2: Micro.Micro<number, string> = Micro.die("Something went wrong")
 
// This will only log defects, not errors
const tapping2 = Micro.tapDefect(task2, (cause) =>
consoleLog(`defect: ${cause}`)
)
 
Micro.runFork(tapping2)
/*
Output:
defect: Something went wrong
*/
ts
import * as Micro from "effect/Micro"
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
// Create an effect that is designed to fail, simulating an occurrence of a network error
const task1: Micro.Micro<number, string> = Micro.fail("NetworkError")
 
// this won't log anything because is not a defect
const tapping1 = 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.
const task2: Micro.Micro<number, string> = Micro.die("Something went wrong")
 
// This will only log defects, not errors
const tapping2 = 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 * as Micro from "effect/Micro"
 
class MyError extends Micro.Error<{ message: string }> {}
 
export const program = Micro.gen(function* () {
// same as yield* Effect.fail(new MyError({ message: "Oh no!" })
yield* new MyError({ message: "Oh no!" })
})
 
Micro.runPromiseExit(program).then(console.log)
/*
Output:
{
_id: 'Either',
_tag: 'Left',
left: (MicroCause.Fail) Error: Oh no!
...stack trace...
}
*/
ts
import * as Micro from "effect/Micro"
 
class MyError extends Micro.Error<{ message: string }> {}
 
export const program = Micro.gen(function* () {
// same as yield* Effect.fail(new MyError({ message: "Oh no!" })
yield* new MyError({ 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 * as Micro from "effect/Micro"
 
// 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(
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 * as Micro from "effect/Micro"
 
// 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(
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 * as Context from "effect/Context"
import * as Micro from "effect/Micro"
 
// Define a service using a unique identifier
class Random extends Context.Tag("MyRandomService")<
Random,
{ readonly next: Micro.Micro<number> } // Operations
>() {}
ts
import * as Context from "effect/Context"
import * as Micro from "effect/Micro"
 
// Define a service using a unique identifier
class Random extends Context.Tag("MyRandomService")<
Random,
{ readonly next: 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
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}`)
})
ts
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: 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 implementation
const runnable = Micro.provideService(program, Random, {
next: Micro.sync(() => Math.random())
})
 
// Execute the program and print the random number
Micro.runPromise(runnable)
/*
Example Output:
random number: 0.8241872233134417
*/
ts
// Provide the Random service implementation
const runnable = Micro.provideService(program, Random, {
next: Micro.sync(() => Math.random())
})
 
// Execute the program and print the random number
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, which represent the release of a resource.
  • Close the scope, releasing all acquired resources and executing any added finalizers.
ts
import * as Micro from "effect/Micro"
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
const program =
// create a new scope
Micro.scopeMake.pipe(
// add finalizer 1
Micro.tap((scope) => scope.addFinalizer(() => consoleLog("finalizer 1"))),
// add finalizer 2
Micro.tap((scope) => scope.addFinalizer(() => consoleLog("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
*/
ts
import * as Micro from "effect/Micro"
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
const program =
// create a new scope
Micro.scopeMake.pipe(
// add finalizer 1
Micro.tap((scope) => scope.addFinalizer(() => consoleLog("finalizer 1"))),
// add finalizer 2
Micro.tap((scope) => scope.addFinalizer(() => consoleLog("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
*/

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 * as Micro from "effect/Micro"
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
const program = Micro.gen(function* () {
yield* Micro.addFinalizer((exit) =>
consoleLog(`finalizer after ${exit._tag}`)
)
return 1
})
 
const runnable = Micro.scoped(program)
 
Micro.runPromise(runnable).then(console.log, console.error)
/*
Output:
finalizer after Right
1
*/
ts
import * as Micro from "effect/Micro"
 
const consoleLog = (message: string) => Micro.sync(() => console.log(message))
 
const program = Micro.gen(function* () {
yield* Micro.addFinalizer((exit) =>
consoleLog(`finalizer after ${exit._tag}`)
)
return 1
})
 
const runnable = Micro.scoped(program)
 
Micro.runPromise(runnable).then(console.log, console.error)
/*
Output:
finalizer after Right
1
*/

Next, let's explore how things behave in the event of a failure:

ts
import * as Micro from "effect/Micro"
 
const consoleLog = (message?: any, ...optionalParams: Array<any>) =>
Micro.sync(() => console.log(message, ...optionalParams))
 
const program = Micro.gen(function* () {
yield* Micro.addFinalizer((exit) =>
consoleLog(`finalizer after ${exit._tag}`)
)
return yield* Micro.fail("Uh oh!")
})
 
const runnable = Micro.scoped(program)
 
Micro.runPromiseExit(runnable).then(console.log)
/*
Output:
finalizer after Left
{ _id: 'Either', _tag: 'Left', left: MicroCause.Fail: Uh oh! }
*/
ts
import * as Micro from "effect/Micro"
 
const consoleLog = (message?: any, ...optionalParams: Array<any>) =>
Micro.sync(() => console.log(message, ...optionalParams))
 
const program = Micro.gen(function* () {
yield* Micro.addFinalizer((exit) =>
consoleLog(`finalizer after ${exit._tag}`)
)
return yield* Micro.fail("Uh oh!")
})
 
const runnable = 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 * as Micro from "effect/Micro"
 
// Define the interface for the resource
interface MyResource {
readonly contents: string
readonly close: () => Promise<void>
}
 
// Simulate getting the resource
const getMyResource = (): Promise<MyResource> =>
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 resource = Micro.acquireRelease(acquire, release)
 
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
*/
ts
import * as Micro from "effect/Micro"
 
// Define the interface for the resource
interface MyResource {
readonly contents: string
readonly close: () => Promise<void>
}
 
// Simulate getting the resource
const getMyResource = (): Promise<MyResource> =>
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 resource = Micro.acquireRelease(acquire, release)
 
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.

Here's an example that demonstrates the usage of acquireUseRelease:

ts
import * as Micro from "effect/Micro"
 
// Define the interface for the resource
interface MyResource {
readonly contents: string
readonly close: () => Promise<void>
}
 
// Simulate getting the resource
const getMyResource = (): Promise<MyResource> =>
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}`))
 
const program = Micro.acquireUseRelease(acquire, use, release)
 
Micro.runPromise(program)
/*
Resource acquired
content is lorem ipsum
Resource released
*/
ts
import * as Micro from "effect/Micro"
 
// Define the interface for the resource
interface MyResource {
readonly contents: string
readonly close: () => Promise<void>
}
 
// Simulate getting the resource
const getMyResource = (): Promise<MyResource> =>
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}`))
 
const program = Micro.acquireUseRelease(acquire, use, release)
 
Micro.runPromise(program)
/*
Resource acquired
content is lorem ipsum
Resource 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 * as Micro from "effect/Micro"
 
const action = Micro.sync(() => console.log("success"))
 
const policy = Micro.scheduleAddDelay(Micro.scheduleRecurs(2), () => 100)
 
const program = Micro.repeat(action, { schedule: policy })
 
Micro.runPromise(program).then((n) => console.log(`repetitions: ${n}`))
/*
Output:
success
success
success
*/
ts
import * as Micro from "effect/Micro"
 
const action = Micro.sync(() => console.log("success"))
 
const policy = Micro.scheduleAddDelay(Micro.scheduleRecurs(2), () => 100)
 
const program = Micro.repeat(action, { schedule: policy })
 
Micro.runPromise(program).then((n) => console.log(`repetitions: ${n}`))
/*
Output:
success
success
success
*/

Failure Example

ts
import * as Micro from "effect/Micro"
 
let count = 0
 
// Define an async effect that simulates an action with possible failures
const action = 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!"))
}
})
 
const policy = Micro.scheduleAddDelay(Micro.scheduleRecurs(2), () => 100)
 
const program = Micro.repeat(action, { schedule: policy })
 
Micro.runPromiseExit(program).then(console.log)
/*
Output:
success
success
failure
{ _id: 'Either', _tag: 'Left', left: MicroCause.Fail: Uh oh! }
*/
ts
import * as Micro from "effect/Micro"
 
let count = 0
 
// Define an async effect that simulates an action with possible failures
const action = 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!"))
}
})
 
const policy = Micro.scheduleAddDelay(Micro.scheduleRecurs(2), () => 100)
 
const program = Micro.repeat(action, { schedule: policy })
 
Micro.runPromiseExit(program).then(console.log)
/*
Output:
success
success
failure
{ _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 * as Micro from "effect/Micro"
import * as Option from "effect/Option"
 
export const dryRun = (
schedule: Micro.MicroSchedule,
maxAttempt: number = 7
): Array<number> => {
let attempt = 1
let elapsed = 0
let duration = schedule(attempt, elapsed)
const out: Array<number> = []
while (Option.isSome(duration) && attempt <= maxAttempt) {
const value = duration.value
attempt++
elapsed += value
out.push(value)
duration = schedule(attempt, elapsed)
}
return out
}
ts
import type * as Micro from "effect/Micro"
import * as Option from "effect/Option"
 
export const dryRun = (
schedule: Micro.MicroSchedule,
maxAttempt: number = 7
): Array<number> => {
let attempt = 1
let elapsed = 0
let duration = schedule(attempt, elapsed)
const out: Array<number> = []
while (Option.isSome(duration) && attempt <= maxAttempt) {
const value = duration.value
attempt++
elapsed += value
out.push(value)
duration = schedule(attempt, elapsed)
}
return out
}

scheduleSpaced

A schedule that recurs continuously, each repetition spaced the specified duration from the last run.

ts
import * as Micro from "effect/Micro"
import { dryRun } from "./dryRun"
 
const policy = Micro.scheduleSpaced(10)
 
console.log(dryRun(policy))
/*
Output:
[
10, 10, 10, 10,
10, 10, 10
]
*/
ts
import * as Micro from "effect/Micro"
import { dryRun } from "./dryRun"
 
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.

ts
import * as Micro from "effect/Micro"
import { dryRun } from "./dryRun"
 
const policy = Micro.scheduleExponential(10)
 
console.log(dryRun(policy))
/*
Output:
[
20, 40, 80,
160, 320, 640,
1280
]
*/
ts
import * as Micro from "effect/Micro"
import { dryRun } from "./dryRun"
 
const policy = 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 * as Micro from "effect/Micro"
import { dryRun } from "./dryRun"
 
const policy = Micro.scheduleUnion(
Micro.scheduleExponential(10),
Micro.scheduleSpaced(300)
)
 
console.log(dryRun(policy))
/*
Output:
[
20, < exponential
40,
80,
160,
300, < spaced
300,
300
]
*/
ts
import * as Micro from "effect/Micro"
import { dryRun } from "./dryRun"
 
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 through the intersection, by recurring only if both schedules want to recur, using the maximum of the two delays between recurrences.

ts
import * as Micro from "effect/Micro"
import { dryRun } from "./dryRun"
 
const policy = Micro.scheduleIntersect(
Micro.scheduleExponential(10),
Micro.scheduleSpaced(300)
)
 
console.log(dryRun(policy))
/*
Output:
[
300, < spaced
300,
300,
300,
320, < exponential
640,
1280
]
*/
ts
import * as Micro from "effect/Micro"
import { dryRun } from "./dryRun"
 
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:

ts
import * as Micro from "effect/Micro"
 
const fib = (n: number): Micro.Micro<number> =>
Micro.suspend(() => {
if (n <= 1) {
return Micro.succeed(n)
}
return fib(n - 1).pipe(Micro.zipWith(fib(n - 2), (a, b) => a + b))
})
 
const fib10Fiber = Micro.fork(fib(10))
ts
import * as Micro from "effect/Micro"
 
const fib = (n: number): Micro.Micro<number> =>
Micro.suspend(() => {
if (n <= 1) {
return Micro.succeed(n)
}
return fib(n - 1).pipe(Micro.zipWith(fib(n - 2), (a, b) => a + b))
})
 
const fib10Fiber = 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 * as Micro from "effect/Micro"
 
const fib = (n: number): Micro.Micro<number> =>
Micro.suspend(() => {
if (n <= 1) {
return Micro.succeed(n)
}
return fib(n - 1).pipe(Micro.zipWith(fib(n - 2), (a, b) => a + b))
})
 
const fib10Fiber = Micro.fork(fib(10))
 
const program = Micro.gen(function* () {
const fiber = yield* fib10Fiber
const n = yield* fiber.join
console.log(n)
})
 
Micro.runPromise(program) // 55
ts
import * as Micro from "effect/Micro"
 
const fib = (n: number): Micro.Micro<number> =>
Micro.suspend(() => {
if (n <= 1) {
return Micro.succeed(n)
}
return fib(n - 1).pipe(Micro.zipWith(fib(n - 2), (a, b) => a + b))
})
 
const fib10Fiber = Micro.fork(fib(10))
 
const program = Micro.gen(function* () {
const fiber = yield* fib10Fiber
const n = 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 * as Micro from "effect/Micro"
 
const fib = (n: number): Micro.Micro<number> =>
Micro.suspend(() => {
if (n <= 1) {
return Micro.succeed(n)
}
return fib(n - 1).pipe(Micro.zipWith(fib(n - 2), (a, b) => a + b))
})
 
const fib10Fiber = Micro.fork(fib(10))
 
const program = Micro.gen(function* () {
const fiber = yield* fib10Fiber
const exit = yield* fiber.await
console.log(exit)
})
 
Micro.runPromise(program) // { _id: 'Either', _tag: 'Right', right: 55 }
ts
import * as Micro from "effect/Micro"
 
const fib = (n: number): Micro.Micro<number> =>
Micro.suspend(() => {
if (n <= 1) {
return Micro.succeed(n)
}
return fib(n - 1).pipe(Micro.zipWith(fib(n - 2), (a, b) => a + b))
})
 
const fib10Fiber = Micro.fork(fib(10))
 
const program = Micro.gen(function* () {
const fiber = yield* fib10Fiber
const exit = 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 * as Micro from "effect/Micro"
 
const program = Micro.gen(function* () {
const fiber = yield* Micro.fork(Micro.forever(Micro.succeed("Hi!")))
const exit = yield* fiber.interrupt
console.log(exit)
})
 
Micro.runPromise(program)
/*
Output
{
_id: 'Either',
_tag: 'Left',
left: MicroCause.Interrupt: interrupted
}
*/
ts
import * as Micro from "effect/Micro"
 
const program = Micro.gen(function* () {
const fiber = yield* Micro.fork(Micro.forever(Micro.succeed("Hi!")))
const exit = 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 * as Micro from "effect/Micro"
 
const task1 = Micro.delay(Micro.fail("task1"), 1_000)
const task2 = Micro.delay(Micro.succeed("task2"), 2_000)
 
const program = Micro.race(task1, task2)
 
Micro.runPromise(program).then(console.log)
/*
Output:
task2
*/
ts
import * as Micro from "effect/Micro"
 
const task1 = Micro.delay(Micro.fail("task1"), 1_000)
const task2 = Micro.delay(Micro.succeed("task2"), 2_000)
 
const program = 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 * as Micro from "effect/Micro"
 
const task1 = Micro.delay(Micro.fail("task1"), 1_000)
const task2 = Micro.delay(Micro.succeed("task2"), 2_000)
 
const program = Micro.race(Micro.either(task1), Micro.either(task2))
 
Micro.runPromise(program).then(console.log)
/*
Output:
{ _id: 'Either', _tag: 'Left', left: 'task1' }
*/
ts
import * as Micro from "effect/Micro"
 
const task1 = Micro.delay(Micro.fail("task1"), 1_000)
const task2 = Micro.delay(Micro.succeed("task2"), 2_000)
 
const program = 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 * as Micro from "effect/Micro"
 
const myEffect = 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 = 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 * as Micro from "effect/Micro"
 
const myEffect = 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 = 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 * as Micro from "effect/Micro"
 
const myEffect = 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 = myEffect.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: 'Either',
_tag: 'Left',
left: (MicroCause.Fail) TimeoutException
...stack trace...
}
*/
ts
import * as Micro from "effect/Micro"
 
const myEffect = 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 = myEffect.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: 'Either',
_tag: 'Left',
left: (MicroCause.Fail) TimeoutException
...stack trace...
}
*/

Calling Effect.interrupt

ts
import * as Micro from "effect/Micro"
 
const program = 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 * as Micro from "effect/Micro"
 
const program = 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 * as Micro from "effect/Micro"
 
const program = 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 #1
start #2
start #3
done #1
{
"_id": "Either",
"_tag": "Left",
"left": {
"_tag": "Interrupt",
"traces": [],
"name": "MicroCause.Interrupt"
}
}
*/
ts
import * as Micro from "effect/Micro"
 
const program = 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 #1
start #2
start #3
done #1
{
"_id": "Either",
"_tag": "Left",
"left": {
"_tag": "Interrupt",
"traces": [],
"name": "MicroCause.Interrupt"
}
}
*/