Creating Effects
Effect provides different ways to create effects, which are units of computation that encapsulate side effects. In this guide, we will cover some of the common methods that you can use to create effects.
In traditional programming, when an error occurs, it is often handled by throwing an exception:
However, throwing errors can be problematic. The type signatures of functions do not indicate that they can throw exceptions, making it difficult to reason about potential errors.
To address this issue, Effect introduces dedicated constructors for creating effects that represent both success and failure: Effect.succeed
and Effect.fail
. These constructors allow you to explicitly handle success and failure cases while leveraging the type system to track errors.
The Effect.succeed
constructor is used to create an effect that will always succeed.
Example (Creating a Successful Effect)
The type of success
is Effect<number, never, never>
, which means:
- It produces a value of type
number
. - It does not generate any errors (
never
indicates no errors). - It requires no additional data or dependencies (
never
indicates no requirements).
When a computation may fail, it’s important to manage failure explicitly. The Effect.fail
constructor allows you to represent an error in a type-safe way.
Example (Creating a Failed Effect)
The type of failure
is Effect<never, Error, never>
, which means:
- It never produces a value (
never
indicates that no successful result will be produced). - It fails with an error, specifically an
Error
. - It requires no additional data or dependencies (
never
indicates no requirements).
Although you can use Error
objects with Effect.fail
, you can also pass strings, numbers, or more complex objects depending on your error management strategy.
Using “tagged” errors (objects with a _tag
field) can help identify error types and works well with standard Effect functions, like Effect.catchTag.
Example (Using Tagged Errors)
With Effect.succeed
and Effect.fail
, you can explicitly handle success and failure cases and the type system will ensure that errors are tracked and accounted for.
Example (Rewriting a Division Function)
Here’s how you can rewrite the divide
function using Effect, making error handling explicit.
In this example, the divide
function indicates in its return type Effect<number, Error>
that the operation can either succeed with a number
or fail with an Error
.
This clear type signature helps ensure that errors are handled properly and that anyone calling the function is aware of the possible outcomes.
Example (Simulating a User Retrieval Operation)
Let’s imagine another scenario where we use Effect.succeed
and Effect.fail
to model a simple user retrieval operation where the user data is hardcoded, which could be useful in testing scenarios or when mocking data:
In this example, exampleUserEffect
, which has the type Effect<User, Error>
, will either produce a User
object or an Error
, depending on whether the user exists in the mocked database.
For a deeper dive into managing errors in your applications, refer to the Error Management Guide.
In JavaScript, you can delay the execution of synchronous computations using “thunks”.
Thunks are useful for delaying the computation of a value until it is needed.
To model synchronous side effects, Effect provides the Effect.sync
and Effect.try
constructors, which accept a thunk.
When working with side effects that are synchronous — meaning they don’t involve asynchronous operations like fetching data from the internet — you can use the Effect.sync
function.
This function is ideal when you are certain these operations won’t produce any errors.
Example (Logging a Message)
In the above example, Effect.sync
is used to defer the side-effect of writing to the console.
Important Notes:
- Execution: The side effect (logging to the console) encapsulated within
program
won’t occur until the effect is explicitly run. This allows you to define side effects at one point in your code and control when they are activated, improving manageability and predictability of side effects in larger applications. - Error Handling: It’s crucial that the function you pass to
Effect.sync
does not throw any errors. If you anticipate potential errors, consider using try instead, which handles errors gracefully.
Handling Unexpected Errors. Despite your best efforts to avoid errors in the function passed to Effect.sync
, if an error does occur, it results in a “defect”.
This defect is not a standard error but indicates a flaw in the logic that was expected to be error-free.
You can think of it similar to an unexpected crash in the program, which can be further managed or logged using tools like Effect.catchAllDefect.
This feature ensures that even unexpected failures in your application are not lost and can be handled appropriately.
In situations where you need to perform synchronous operations that might fail, such as parsing JSON, you can use the Effect.try
constructor from the Effect library.
This constructor is designed to handle operations that could throw exceptions by capturing those exceptions and transforming them into manageable errors within the Effect framework.
Example (Safe JSON Parsing)
Suppose you have a function that attempts to parse a JSON string. This operation can fail and throw an error if the input string is not properly formatted as JSON:
In this example:
parse
is a function that creates an effect encapsulating the JSON parsing operation.- If
JSON.parse(input)
throws an error due to invalid input,Effect.try
catches this error and the effect represented byprogram
will fail with anUnknownException
. This ensures that errors are not silently ignored but are instead handled within the structured flow of effects.
You might want to transform the caught exception into a more specific error or perform additional operations when catching an error. Effect.try
supports an overload that allows you to specify how caught exceptions should be transformed:
Example (Custom Error Handling)
You can think of this as a similar pattern to the traditional try-catch block in JavaScript:
In traditional programming, we often use Promise
s to handle asynchronous computations. However, dealing with errors in promises can be problematic. By default, Promise<Value>
only provides the type Value
for the resolved value, which means errors are not reflected in the type system. This limits the expressiveness and makes it challenging to handle and track errors effectively.
To overcome these limitations, Effect introduces dedicated constructors for creating effects that represent both success and failure in an asynchronous context: Effect.promise
and Effect.tryPromise
. These constructors allow you to explicitly handle success and failure cases while leveraging the type system to track errors.
This constructor is similar to a regular Promise
, where you’re confident that the asynchronous operation will always succeed.
It allows you to create an Effect
that represents successful completion without considering potential errors. However, it’s essential to ensure that the underlying Promise never rejects.
Example (Delayed Message)
The program
value has the type Effect<string, never, never>
and can be interpreted as an effect that:
- succeeds with a value of type
string
- does not produce any expected error (
never
) - does not require any context (
never
)
If, despite precautions, the thunk passed to Effect.promise
does reject, an Effect
containing a defect is created, similar to what happens when using the Effect.die function.
Unlike Effect.promise
, this constructor is suitable when the underlying Promise
might reject.
It provides a way to catch errors and handle them appropriately.
By default if an error occurs, it will be caught and propagated to the error channel as as an UnknownException
.
Example (Fetching a TODO Item)
The program
value has the type Effect<Response, UnknownException, never>
and can be interpreted as an effect that:
- succeeds with a value of type
Response
- might produce an error (
UnknownException
) - does not require any context (
never
)
If you want more control over what gets propagated to the error channel, you can use an overload of Effect.tryPromise
that takes a remapping function:
Example (Custom Error Handling)
Sometimes you have to work with APIs that don’t support async/await
or Promise
and instead use the callback style.
To handle callback-based APIs, Effect provides the Effect.async
constructor.
Example (Wrapping a Callback API)
Let’s wrap the readFile
function from Node.js’s fs
module into an Effect-based API (make sure @types/node
is installed):
In the above example, we manually annotate the types when calling Effect.async
:
because TypeScript cannot infer the type parameters for a callback
based on the return value inside the callback body. Annotating the types ensures that the values provided to resume
match the expected types.
The resume
function inside Effect.async
should be called exactly once. Calling it more than once will result in the extra calls being ignored.
Example (Ignoring Subsequent resume
Calls)
For more advanced use cases, resume
can optionally return an Effect
that will be executed if the fiber running this effect is interrupted. This can be useful in scenarios where you need to handle resource cleanup if the operation is interrupted.
Example (Handling Interruption with Cleanup)
In this example:
- The
writeFileWithCleanup
function writes data to a file. - If the fiber running this effect is interrupted, the cleanup effect (which deletes the file) is executed.
- This ensures that resources like open file handles are cleaned up properly when the operation is canceled.
If the operation you’re wrapping supports interruption, the resume
function can receive an AbortSignal
to handle interruption requests directly.
Example (Handling Interruption with AbortSignal
)
Effect.suspend
is used to delay the creation of an effect.
It allows you to defer the evaluation of an effect until it is actually needed.
The Effect.suspend
function takes a thunk that represents the effect, and it wraps it in a suspended effect.
Syntax
Let’s explore some common scenarios where Effect.suspend
proves useful.
When you want to defer the evaluation of an effect until it is required. This can be useful for optimizing the execution of effects, especially when they are not always needed or when their computation is expensive.
Also, when effects with side effects or scoped captures are created, use Effect.suspend
to re-execute on each invocation.
Example (Lazy Evaluation with Side Effects)
In this example, bad
is the result of calling Effect.succeed(i++)
a single time, which increments the scoped variable but returns its original value. Effect.runSync(bad)
does not result in any new computation, because Effect.succeed(i++)
has already been called. On the other hand, each time Effect.runSync(good)
is called, the thunk passed to Effect.suspend()
will be executed, outputting the scoped variable’s most recent value.
Effect.suspend
is helpful in managing circular dependencies between effects, where one effect depends on another, and vice versa.
For example it’s fairly common for Effect.suspend
to be used in recursive functions to escape an eager call.
Example (Recursive Fibonacci)
The blowsUp
function creates a recursive Fibonacci sequence without deferring execution. Each call to blowsUp
triggers further immediate recursive calls, rapidly increasing the JavaScript call stack size.
Conversely, allGood
avoids stack overflow by using Effect.suspend
to defer the recursive calls. This mechanism doesn’t immediately execute the recursive effects but schedules them to be run later, thus keeping the call stack shallow and preventing a crash.
In situations where TypeScript struggles to unify the returned effect type, Effect.suspend
can be employed to resolve this issue.
Example (Using Effect.suspend
to Help TypeScript Infer Types)
The table provides a summary of the available constructors, along with their input and output types, allowing you to choose the appropriate function based on your needs.
API | Given | Result |
---|---|---|
succeed | A | Effect<A> |
fail | E | Effect<never, E> |
sync | () => A | Effect<A> |
try | () => A | Effect<A, UnknownException> |
try (overload) | () => A , unknown => E | Effect<A, E> |
promise | () => Promise<A> | Effect<A> |
tryPromise | () => Promise<A> | Effect<A, UnknownException> |
tryPromise (overload) | () => Promise<A> , unknown => E | Effect<A, E> |
async | (Effect<A, E> => void) => void | Effect<A, E> |
suspend | () => Effect<A, E, R> | Effect<A, E, R> |
For the complete list of constructors, visit the Effect Constructors Documentation.