Creating Effects
On this page
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.
Why Not Throw Errors?
In traditional programming, when an error occurs, it is often handled by throwing an exception:
ts
constdivide = (a : number,b : number): number => {if (b === 0) {throw newError ("Cannot divide by zero")}returna /b }
ts
constdivide = (a : number,b : number): number => {if (b === 0) {throw newError ("Cannot divide by zero")}returna /b }
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.
succeed
To create an effect that represents a successful computation, you can use the Effect.succeed
constructor. It takes a value as input and produces an effect that succeeds with that value. Here's an example:
ts
import {Effect } from "effect"constsuccess =Effect .succeed (42)
ts
import {Effect } from "effect"constsuccess =Effect .succeed (42)
The success
value has the type Effect<number>
and can be interpreted as an effect that:
- succeeds with a value of type
number
- does not produce any expected error (
never
) - does not require any context (
never
)
fail
To create an effect that represents a failure, you can use the Effect.fail
constructor. It takes an error value as input and produces an effect that fails with that error. Here's an example:
ts
import {Effect } from "effect"constfailure =Effect .fail ("my error")
ts
import {Effect } from "effect"constfailure =Effect .fail ("my error")
The failure
value has the type Effect<never, string, never>
and can be interpreted as an effect that:
- does not produce any value (
never
) - fails with an expected error of type
string
- does not require any context (
never
)
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.
Let's see an example of rewriting the divide
function using Effect to make the error handling explicit:
ts
import {Effect } from "effect"constdivide = (a : number,b : number):Effect .Effect <number,Error > =>b === 0?Effect .fail (newError ("Cannot divide by zero")):Effect .succeed (a /b )
ts
import {Effect } from "effect"constdivide = (a : number,b : number):Effect .Effect <number,Error > =>b === 0?Effect .fail (newError ("Cannot divide by zero")):Effect .succeed (a /b )
In this example, the divide
function explicitly indicates that it can produce an effect that either fails with an Error
or succeeds with a number
value. The type signature makes it clear how errors are handled and ensures that callers are aware of the possible outcomes.
Modeling Synchronous Effects
In JavaScript, you can delay the execution of synchronous computations using "thunks".
A "thunk" is a function that takes no arguments and may return some value.
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.
sync
If you're working with synchronous side effects and you're confident that they will never throw errors, you can use the Effect.sync
constructor:
ts
import {Effect } from "effect"constlog = (message : string) =>Effect .sync (() => {console .log (message ) // side effect})constprogram =log ("Hello, World!")
ts
import {Effect } from "effect"constlog = (message : string) =>Effect .sync (() => {console .log (message ) // side effect})constprogram =log ("Hello, World!")
In the above example, Effect.sync
is used to defer the side-effect of writing to the console.
The program
value has the type Effect<void>
since the thunk doesn't return any value.
Note that nothing is logged to the console yet when you inspect it.
This is because program
represents the action of writing to the console, but nothing happens until you explicitly run your program.
Effect.sync
should never throw errors.try
If the synchronous computation within the thunk may throw an error, you can use the Effect.try
constructor.
If an error is caught, it will be propagated to the error channel as UnknownException
:
ts
import {Effect } from "effect"constparse = (input : string) =>Effect .try (() =>JSON .parse (input ) // JSON.parse may throw for bad input)constprogram =parse ("")
ts
import {Effect } from "effect"constparse = (input : string) =>Effect .try (() =>JSON .parse (input ) // JSON.parse may throw for bad input)constprogram =parse ("")
If you want more control over what gets propagated to the error channel, you can use an overload of Effect.try
that takes a remapping function:
ts
import {Effect } from "effect"constparse = (input : string) =>Effect .try ({try : () =>JSON .parse (input ), // JSON.parse may throw for bad inputcatch : (unknown ) => newError (`something went wrong ${unknown }`) // remap the error})constprogram =parse ("")
ts
import {Effect } from "effect"constparse = (input : string) =>Effect .try ({try : () =>JSON .parse (input ), // JSON.parse may throw for bad inputcatch : (unknown ) => newError (`something went wrong ${unknown }`) // remap the error})constprogram =parse ("")
You can think of this as a similar pattern to the traditional try-catch block in JavaScript:
ts
try {return JSON.parse(input)} catch (unknown) {throw new Error(`something went wrong ${unknown}`)}
ts
try {return JSON.parse(input)} catch (unknown) {throw new Error(`something went wrong ${unknown}`)}
Modeling Asynchronous Effects
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.
promise
If you're working with asynchronous side effects that return a Promise
and you're confident that the Promise
will never reject, you can use the Effect.promise
constructor:
ts
import {Effect } from "effect"constdelay = (message : string) =>Effect .promise <string>(() =>newPromise ((resolve ) => {setTimeout (() => {resolve (message )}, 2000)}))constprogram =delay ("Async operation completed successfully!")
ts
import {Effect } from "effect"constdelay = (message : string) =>Effect .promise <string>(() =>newPromise ((resolve ) => {setTimeout (() => {resolve (message )}, 2000)}))constprogram =delay ("Async operation completed successfully!")
The program
value has the type Effect<string>
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
)
The Promise
within the thunk passed to Effect.promise
should never
reject.
tryPromise
If the Promise
within the thunk may reject, you can use the Effect.tryPromise
constructor.
If an error is caught, it will be propagated to the error channel as UnknownException
:
ts
import {Effect } from "effect"constgetTodo = (id : number) =>Effect .tryPromise (() =>fetch (`https://jsonplaceholder.typicode.com/todos/${id }`))constprogram =getTodo (1)
ts
import {Effect } from "effect"constgetTodo = (id : number) =>Effect .tryPromise (() =>fetch (`https://jsonplaceholder.typicode.com/todos/${id }`))constprogram =getTodo (1)
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:
ts
import {Effect } from "effect"constgetTodo = (id : number) =>Effect .tryPromise ({try : () =>fetch (`https://jsonplaceholder.typicode.com/todos/${id }`),catch : (unknown ) => newError (`something went wrong ${unknown }`) // remap the error})constprogram =getTodo (1)
ts
import {Effect } from "effect"constgetTodo = (id : number) =>Effect .tryPromise ({try : () =>fetch (`https://jsonplaceholder.typicode.com/todos/${id }`),catch : (unknown ) => newError (`something went wrong ${unknown }`) // remap the error})constprogram =getTodo (1)
From a callback
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.
For example, let's wrap the readFile
async API from the Node.js fs
module with Effect (ensure you have @types/node
installed):
ts
import {Effect } from "effect"import * asNodeFS from "node:fs"constreadFile = (filename : string) =>Effect .async <Buffer ,Error >((resume ) => {NodeFS .readFile (filename , (error ,data ) => {if (error ) {resume (Effect .fail (error ))} else {resume (Effect .succeed (data ))}})})constprogram =readFile ("todos.txt")
ts
import {Effect } from "effect"import * asNodeFS from "node:fs"constreadFile = (filename : string) =>Effect .async <Buffer ,Error >((resume ) => {NodeFS .readFile (filename , (error ,data ) => {if (error ) {resume (Effect .fail (error ))} else {resume (Effect .succeed (data ))}})})constprogram =readFile ("todos.txt")
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.
You can seamlessly mix synchronous and asynchronous code within the Effect framework. Everything becomes an Effect, enabling you to handle different types of side effects in a unified way.
Suspended Effects
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.
ts
const suspendedEffect = Effect.suspend(() => effect)
ts
const suspendedEffect = Effect.suspend(() => effect)
Let's explore some common scenarios where Effect.suspend
proves useful:
-
Lazy Evaluation. 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.tsimport {Effect } from "effect"leti = 0constbad =Effect .succeed (i ++)constgood =Effect .suspend (() =>Effect .succeed (i ++))console .log (Effect .runSync (bad )) // Output: 0console .log (Effect .runSync (bad )) // Output: 0console .log (Effect .runSync (good )) // Output: 1console .log (Effect .runSync (good )) // Output: 2tsimport {Effect } from "effect"leti = 0constbad =Effect .succeed (i ++)constgood =Effect .suspend (() =>Effect .succeed (i ++))console .log (Effect .runSync (bad )) // Output: 0console .log (Effect .runSync (bad )) // Output: 0console .log (Effect .runSync (good )) // Output: 1console .log (Effect .runSync (good )) // Output: 2In this example,
Effect.succeed(i++)
creates a new numeric value and consistently returns the same number. On the other hand,Effect.suspend(() => Effect.succeed(i++))
generates a new number with each invocation.This example utilizes
Effect.runSync
to execute effects and display their results (refer to Running Effects for more details). -
Handling Circular Dependencies.
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 forEffect.suspend
to be used in recursive functions to escape an eager call. For instance:tsimport {Effect } from "effect"constblowsUp = (n : number):Effect .Effect <number> =>n < 2?Effect .succeed (1):Effect .zipWith (blowsUp (n - 1),blowsUp (n - 2), (a ,b ) =>a +b )// console.log(Effect.runSync(blowsUp(32))) // crash: JavaScript heap out of memoryconstallGood = (n : number):Effect .Effect <number> =>n < 2?Effect .succeed (1):Effect .zipWith (Effect .suspend (() =>allGood (n - 1)),Effect .suspend (() =>allGood (n - 2)),(a ,b ) =>a +b )console .log (Effect .runSync (allGood (32))) // Output: 3524578tsimport {Effect } from "effect"constblowsUp = (n : number):Effect .Effect <number> =>n < 2?Effect .succeed (1):Effect .zipWith (blowsUp (n - 1),blowsUp (n - 2), (a ,b ) =>a +b )// console.log(Effect.runSync(blowsUp(32))) // crash: JavaScript heap out of memoryconstallGood = (n : number):Effect .Effect <number> =>n < 2?Effect .succeed (1):Effect .zipWith (Effect .suspend (() =>allGood (n - 1)),Effect .suspend (() =>allGood (n - 2)),(a ,b ) =>a +b )console .log (Effect .runSync (allGood (32))) // Output: 3524578 -
Unifying Return Type. In situations where TypeScript struggles to unify the returned effect type,
Effect.suspend
can be employed to resolve this issue. For example:tsimport {Effect } from "effect"constugly = (a : number,b : number) =>b === 0?Effect .fail (newError ("Cannot divide by zero")):Effect .succeed (a /b )constnice = (a : number,b : number) =>Effect .suspend (() =>b === 0?Effect .fail (newError ("Cannot divide by zero")):Effect .succeed (a /b ))tsimport {Effect } from "effect"constugly = (a : number,b : number) =>b === 0?Effect .fail (newError ("Cannot divide by zero")):Effect .succeed (a /b )constnice = (a : number,b : number) =>Effect .suspend (() =>b === 0?Effect .fail (newError ("Cannot divide by zero")):Effect .succeed (a /b ))
Cheatsheet
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.
Function | Given | To |
---|---|---|
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> |
You can find the complete list of constructors here.
Now that we know how to create effects, it's time to learn how to run them. Check out the next guide on Running Effects to find out more.