Effect offers a convenient syntax, similar to async/await, to write effectful code using generators.
Understanding Effect.gen
The Effect.gen utility simplifies the task of writing effectful code by utilizing JavaScript’s generator functions. This method helps your code appear and behave more like traditional synchronous code, which enhances both readability and error management.
Example (Performing Transactions with Discounts)
Let’s explore a practical program that performs a series of data transformations commonly found in application logic:
1
import { Effect } from"effect"
2
3
// Function to add a small service charge to a transaction amount
It’s important to note that although the code appears similar, the two programs are not identical. The purpose of comparing them side by side is just to highlight the resemblance in how they are written.
Embracing Control Flow
One significant advantage of using Effect.gen in conjunction with generators is its capability to employ standard control flow constructs within the generator function. These constructs include if/else, for, while, and other branching and looping mechanisms, enhancing your ability to express complex control flow logic in your code.
Example (Using Control Flow)
1
import { Effect } from"effect"
2
3
constcalculateTax= (
4
amount:number,
5
taxRate:number,
6
):Effect.Effect<number, Error> =>
7
taxRate >0
8
? Effect.succeed((amount * taxRate) /100)
9
: Effect.fail(newError("Invalid tax rate"))
10
11
constprogram= Effect.gen(function* () {
12
let i =1
13
14
while (true) {
15
if (i ===10) {
16
break// Break the loop when counter reaches 10
17
} else {
18
if (i %2===0) {
19
// Calculate tax for even numbers
20
console.log(yield*calculateTax(100, i))
21
}
22
i++
23
continue
24
}
25
}
26
})
27
28
await Effect.runPromise(program) // => undefined
29
/*
30
Output:
31
2
32
4
33
6
34
8
35
*/
How to Raise Errors
The Effect.gen API lets you integrate error handling directly into your workflow by yielding failed effects.
You can introduce errors with Effect.fail, as shown in the example below.
Example (Introducing an Error into the Flow)
1
import { Effect, Console } from"effect"
2
3
consttask1= Console.log("task1...")
4
consttask2= Console.log("task2...")
5
6
constprogram= Effect.gen(function* () {
7
// Perform some tasks
8
yield* task1
9
yield* task2
10
// Introduce an error
11
returnyield* Effect.fail("Something went wrong!")
12
})
13
14
try {
15
await Effect.runPromise(program)
16
} catch (e) {
17
console.error(e)
18
/*
19
Output:
20
task1...
21
task2...
22
*/
23
e // => "Something went wrong!"
24
}
The Role of Short-Circuiting
When working with Effect.gen, it is important to understand how it handles errors.
This API will stop execution at the first error it encounters and return that error.
How does this affect your code? If you have several operations in sequence, once any one of them fails, the remaining operations will not run, and the error will be returned.
In simpler terms, if something fails at any point, the program will stop right there and deliver the error to you.
If you don’t want to stop on an error, you can use the Effect.result method to encapsulate the error in the Result data type: see the examples of managing expected errors.
Example (Halting Execution at the First Error)
1
import { Effect, Console } from"effect"
2
3
consttask1= Console.log("task1...")
4
consttask2= Console.log("task2...")
5
constfailure= Effect.fail("Something went wrong!")
Even though execution never reaches code after a failure, TypeScript may still assume that the code below the error is reachable unless you explicitly return after the failure.
For example, consider the following scenario where you want to narrow the type of a variable:
Example (Type Narrowing without Explicit Return)
1
import { Effect } from"effect"
2
3
typeUser= {
4
readonlyname:string
5
}
6
7
// Imagine this function checks a database or an external service
returnyield* Effect.fail(`User with id ${id} not found`)
16
}
17
18
// Now TypeScript knows that 'user' is not undefined
19
return`Hello, ${user.name}!`
20
})
21
}
22
23
greetUser.length// => 1
Passing this
In some cases, you might need to pass a reference to the current object (this) into the body of your generator function.
You can achieve this by utilizing an overload that accepts the reference as the first argument:
Example (Passing this to Generator)
1
import { Effect } from"effect"
2
3
classMyClass {
4
readonlylocal=1
5
compute= Effect.gen({ self: this }, function* () {