Using Generators in Effect

On this page

In the previous sections, we learned how to create effects and execute them. Now, it's time to write our first simple program.

Effect offers a convenient syntax, similar to async/await, to write effectful code using generators.

The use of generators is an optional feature in Effect. If you find generators unfamiliar or prefer a different coding style, you can explore the documentation about Building Pipelines in Effect.

Understanding Effect.gen

The Effect.gen utility simplifies the task of writing effectful code by utilizing JavaScript's generator functions. This method helps your code appear and behave more like traditional synchronous code, which enhances both readability and error management.

Let's explore a practical program that performs a series of data transformations commonly found in application logic:

ts
import { Effect } from "effect"
 
// Function to add a small service charge to a transaction amount
const addServiceCharge = (amount: number) => amount + 1
 
// Function to apply a discount safely to a transaction amount
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, Error> =>
discountRate === 0
? Effect.fail(new Error("Discount rate cannot be zero"))
: Effect.succeed(total - (total * discountRate) / 100)
 
// Simulated asynchronous task to fetch a transaction amount from a database
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
 
// Simulated asynchronous task to fetch a discount rate from a configuration file
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
 
// Assembling the program using a generator function
const program = Effect.gen(function* () {
// Retrieve the transaction amount
const transactionAmount = yield* fetchTransactionAmount
 
// Retrieve the discount rate
const discountRate = yield* fetchDiscountRate
 
// Calculate discounted amount
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
 
// Apply service charge
const finalAmount = addServiceCharge(discountedAmount)
 
// Return the total amount after applying the charge
return `Final amount to charge: ${finalAmount}`
})
 
// Execute the program and log the result
Effect.runPromise(program).then(console.log) // Output: "Final amount to charge: 96"
ts
import { Effect } from "effect"
 
// Function to add a small service charge to a transaction amount
const addServiceCharge = (amount: number) => amount + 1
 
// Function to apply a discount safely to a transaction amount
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, Error> =>
discountRate === 0
? Effect.fail(new Error("Discount rate cannot be zero"))
: Effect.succeed(total - (total * discountRate) / 100)
 
// Simulated asynchronous task to fetch a transaction amount from a database
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
 
// Simulated asynchronous task to fetch a discount rate from a configuration file
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
 
// Assembling the program using a generator function
const program = Effect.gen(function* () {
// Retrieve the transaction amount
const transactionAmount = yield* fetchTransactionAmount
 
// Retrieve the discount rate
const discountRate = yield* fetchDiscountRate
 
// Calculate discounted amount
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
 
// Apply service charge
const finalAmount = addServiceCharge(discountedAmount)
 
// Return the total amount after applying the charge
return `Final amount to charge: ${finalAmount}`
})
 
// Execute the program and log the result
Effect.runPromise(program).then(console.log) // Output: "Final amount to charge: 96"

Key steps to follow when using Effect.gen:

  • Wrap your logic in Effect.gen
  • Use yield* to handle effects
  • Return the final result

The generator API is only available when using the downlevelIteration flag or with a target of "es2015" or higher in your tsconfig.json file

Comparing Effect.gen with async/await

If you are familiar with async/await, you may notice that the flow of writing code is similar.

Let's compare the two approaches:


ts
const addServiceCharge = (amount: number) => amount + 1
 
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, Error> =>
discountRate === 0
? Effect.fail(new Error("Discount rate cannot be zero"))
: Effect.succeed(total - (total * discountRate) / 100)
 
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
 
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
 
export const program = Effect.gen(function* () {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})
ts
const addServiceCharge = (amount: number) => amount + 1
 
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, Error> =>
discountRate === 0
? Effect.fail(new Error("Discount rate cannot be zero"))
: Effect.succeed(total - (total * discountRate) / 100)
 
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
 
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
 
export const program = Effect.gen(function* () {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

It's important to note that although the code appears similar, the two programs are not identical. The purpose of comparing them side by side is just to highlight the resemblance in how they are written.

Embracing Control Flow

One significant advantage of using Effect.gen in conjunction with generators is its capability to employ standard control flow constructs within the generator function. These constructs include if/else, for, while, and other branching and looping mechanisms, enhancing your ability to express complex control flow logic in your code.

ts
import { Effect } from "effect"
 
const calculateTax = (
amount: number,
taxRate: number
): Effect.Effect<number, Error> =>
taxRate > 0
? Effect.succeed((amount * taxRate) / 100)
: Effect.fail(new Error("Invalid tax rate"))
 
const program = Effect.gen(function* () {
let i = 1
 
while (true) {
if (i === 10) {
break // Break the loop when counter reaches 10
} else {
if (i % 2 === 0) {
// Calculate tax for even numbers
console.log(yield* calculateTax(100, i))
}
i++
continue
}
}
})
 
Effect.runPromise(program)
/*
Output:
2
4
6
8
*/
ts
import { Effect } from "effect"
 
const calculateTax = (
amount: number,
taxRate: number
): Effect.Effect<number, Error> =>
taxRate > 0
? Effect.succeed((amount * taxRate) / 100)
: Effect.fail(new Error("Invalid tax rate"))
 
const program = Effect.gen(function* () {
let i = 1
 
while (true) {
if (i === 10) {
break // Break the loop when counter reaches 10
} else {
if (i % 2 === 0) {
// Calculate tax for even numbers
console.log(yield* calculateTax(100, i))
}
i++
continue
}
}
})
 
Effect.runPromise(program)
/*
Output:
2
4
6
8
*/

Raising Errors

The Effect.gen API allows you to incorporate error handling directly into your program flow by yielding failed effects. This mechanism, achieved through Effect.fail, is demonstrated in the example below:

ts
import { Effect } from "effect"
 
const program = Effect.gen(function* () {
console.log("Task1...")
console.log("Task2...")
// Introduce an error into the flow
yield* Effect.fail("Something went wrong!")
})
 
Effect.runPromiseExit(program).then(console.log)
/*
Output:
Task1...
Task2...
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Fail', failure: 'Something went wrong!' }
}
*/
ts
import { Effect } from "effect"
 
const program = Effect.gen(function* () {
console.log("Task1...")
console.log("Task2...")
// Introduce an error into the flow
yield* Effect.fail("Something went wrong!")
})
 
Effect.runPromiseExit(program).then(console.log)
/*
Output:
Task1...
Task2...
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Fail', failure: 'Something went wrong!' }
}
*/

The Role of Short-Circuiting

When working with the Effect.gen API, it's important to understand how it manages errors. This API is designed to short-circuit the execution upon encountering the first error.

What does this mean for you as a developer? Well, let's say you have a chain of operations or a collection of effects to be executed in sequence. If any error occurs during the execution of one of these effects, the remaining computations will be skipped, and the error will be propagated to the final result.

In simpler terms, the short-circuiting behavior ensures that if something goes wrong at any step of your program it will immediately stop and return the error to let you know that something went wrong.

ts
import { Effect } from "effect"
 
const program = Effect.gen(function* () {
console.log("Task1...")
console.log("Task2...")
yield* Effect.fail("Something went wrong!")
console.log("This won't be executed")
})
 
Effect.runPromise(program).then(console.log, console.error)
/*
Output:
Task1...
Task2...
(FiberFailure) Error: Something went wrong!
*/
ts
import { Effect } from "effect"
 
const program = Effect.gen(function* () {
console.log("Task1...")
console.log("Task2...")
yield* Effect.fail("Something went wrong!")
console.log("This won't be executed")
})
 
Effect.runPromise(program).then(console.log, console.error)
/*
Output:
Task1...
Task2...
(FiberFailure) Error: Something went wrong!
*/

If you want to dive deeper into effective error handling with Effect, you can explore the "Error Management" section.

Passing this

In some cases, you might need to pass a reference to the current object (this) into the body of your generator function. You can achieve this by utilizing an overload that accepts the reference as the first argument:

ts
import { Effect } from "effect"
 
class MyService {
readonly local = 1
compute = Effect.gen(this, function* () {
return yield* Effect.succeed(this.local + 1)
})
}
 
console.log(Effect.runSync(new MyService().compute)) // Output: 2
ts
import { Effect } from "effect"
 
class MyService {
readonly local = 1
compute = Effect.gen(this, function* () {
return yield* Effect.succeed(this.local + 1)
})
}
 
console.log(Effect.runSync(new MyService().compute)) // Output: 2

Adapter

You may still come across some code snippets that use an adapter, typically indicated by _ or $ symbols.

In earlier versions of TypeScript, the generator "adapter" function was necessary to ensure correct type inference within generators. This adapter was used to facilitate the interaction between TypeScript's type system and generator functions.

ts
import { Effect } from "effect"
 
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
 
// Older usage with an adapter for proper type inference
const programWithAdapter = Effect.gen(function* (_ /* <-- Adapter */) {
const transactionAmount = yield* _(fetchTransactionAmount)
})
 
// Current usage without an adapter
const program = Effect.gen(function* () {
const transactionAmount = yield* fetchTransactionAmount
})
ts
import { Effect } from "effect"
 
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
 
// Older usage with an adapter for proper type inference
const programWithAdapter = Effect.gen(function* (_ /* <-- Adapter */) {
const transactionAmount = yield* _(fetchTransactionAmount)
})
 
// Current usage without an adapter
const program = Effect.gen(function* () {
const transactionAmount = yield* fetchTransactionAmount
})

With advances in TypeScript (v5.5+), the adapter is no longer necessary for type inference. While it remains in the codebase for backward compatibility, it is anticipated to be removed in the upcoming major release of Effect.