Docs
Creating Effects

Effect

The Effect data type was designed to be a completely type-safe representation of any program.

Conceptually, we can begin to build a mental model of the Effect data type by thinking of it as a function that takes some Context as input and when executed can either fail with some Error or succeed with some Value.

ts
declare const Effect: (context: Context) => Either<Error, Value>
ts
declare const Effect: (context: Context) => Either<Error, Value>

Note: this mental model should be used for conceptual purposes only - the Effect type is much more powerful than just a function.

The Effect type is parameterized by three generic type parameters:

Effect<Context, Failure, Success>

which have the following meaning:

  • Context: Represents the contextual data which is required by the Effect in order to be executed

  • Failure: Represents expected failures that can occur when an Effect is executed

  • Success: Represents the type of the value that an Effect can succeed with when executed

Throughout the Effect ecosystem, you will often see Effect's type parameters abbreviated as R, E, and A respectively. This is just shorthand for Requirements, Error, and the success value of type A.

You can think of each of these type parameters as separate "channels" within the Effect data type. Your program can interact with each of these "channels" during its execution.

Creating Effects

Effect provides a variety of constructors which can be used to represent programs that succeed and return some value.

In the sections below, we walk through some of the more common use cases.

From a Value

To create an Effect from a value, you can use the succeed constructor:

ts
import * as Effect from "@effect/io/Effect"
 
export const fromValue = Effect.succeed("Hello, World!")
const fromValue: Effect.Effect<never, never, string>
ts
import * as Effect from "@effect/io/Effect"
 
export const fromValue = Effect.succeed("Hello, World!")
const fromValue: Effect.Effect<never, never, string>

By inspecting the type of fromValue, we can see it is Effect<never, never, string>. This type can be interpreted as:

An effect that does not require any context, does not produce any expected failures, and succeeds with a value of type string

From a Thunk

Generally, to defer a synchronous computation in JavaScript we can use a "thunk".

A "thunk" is a function that takes no arguments and may return some value.

Thunks are primarily useful for delaying the computation of a value until the result of said computation is actually needed.

Sometimes you may want to construct an Effect from a "thunk". This can be particularly useful when dealing with:

  1. Synchronous computations that have side-effects (i.e. logging something to the console)
  2. Expensive computations that are expensive to evaluate

To create an Effect from a "thunk", we can use the sync constructor:

ts
import * as Effect from "@effect/io/Effect"
 
export const logHelloWorld = Effect.sync(() => {
const logHelloWorld: Effect.Effect<never, never, number>
console.log("Hello, World!")
return 42
})
ts
import * as Effect from "@effect/io/Effect"
 
export const logHelloWorld = Effect.sync(() => {
const logHelloWorld: Effect.Effect<never, never, number>
console.log("Hello, World!")
return 42
})

In the above example, we use Effect.sync to defer our side-effecting call to the console object until our program is actually executed.

We can also observe that the value that will be returned when this program is executed is of type number since we return the value 42 from our "thunk".

From a Promise

When dealing with asynchronous side effects that return a Promise you can use the promise function.

ts
import * as Effect from "@effect/io/Effect"
 
export const fetchFirstTodo = Effect.promise(() =>
const fetchFirstTodo: Effect.Effect<never, never, Response>
fetch("https://jsonplaceholder.typicode.com/todos/1")
)
ts
import * as Effect from "@effect/io/Effect"
 
export const fetchFirstTodo = Effect.promise(() =>
const fetchFirstTodo: Effect.Effect<never, never, Response>
fetch("https://jsonplaceholder.typicode.com/todos/1")
)

Once again inspecting the return type we can see that it is Effect<never, never, Response>.

From a Callback

Sometimes you have to deal with APIs that do not support async/await or Promise, but are instead implemented in the "old-school" callback style. To support callback-based APIs, Effect provides the async constructor.

Note: Although callback-based APIs may be considered "old-school", they are inherently more powerful than Promise-based APIs. This is because they are both faster than Promise-based APIs as well as more precise in handling failures.

As an example, we can demonstrate wrapping readFile from the NodeJS fs module with Effect:

ts
import * as Effect from "@effect/io/Effect"
import * as NodeFS from "node:fs"
 
export const readTodos = Effect.async<never, NodeJS.ErrnoException, Buffer>(
const readTodos: Effect.Effect<never, NodeJS.ErrnoException, Buffer>
(resume) => {
NodeFS.readFile("todos.txt", (error, data) => {
if (error) {
resume(Effect.fail(error))
} else {
resume(Effect.succeed(data))
}
})
}
)
ts
import * as Effect from "@effect/io/Effect"
import * as NodeFS from "node:fs"
 
export const readTodos = Effect.async<never, NodeJS.ErrnoException, Buffer>(
const readTodos: Effect.Effect<never, NodeJS.ErrnoException, Buffer>
(resume) => {
NodeFS.readFile("todos.txt", (error, data) => {
if (error) {
resume(Effect.fail(error))
} else {
resume(Effect.succeed(data))
}
})
}
)

Something to note in the above example is that we were actually forced to manually annotate the types when calling Effect.async. Unfortunately, TypeScript is not capable of inferring the type parameters for a callback based on what is returned inside the body of the callback. This limitation of TypeScript is the one minor drawback of using callback-based APIs. Annotating the types of Effect.async will ensure that the values provided to resume match the expected types.

Another thing to note is that we probably don't want to have to call the Effect.async constructor everywhere that we use callback-based APIs. Therefore, it is a very common pattern to define helpers which wrap external APIs into an Effect-based API.

For example, we can refactor the previous code into:

ts
import * as Effect from "@effect/io/Effect"
import * as NodeFS from "node:fs"
 
export const readFile = (
path: string
): Effect.Effect<never, NodeJS.ErrnoException, Buffer> =>
Effect.async((resume) => {
NodeFS.readFile(path, (error, data) => {
if (error) {
resume(Effect.fail(error))
} else {
resume(Effect.succeed(data))
}
})
})
 
export const readTodos = readFile("todos.txt")
const readTodos: Effect.Effect<never, NodeJS.ErrnoException, Buffer>
ts
import * as Effect from "@effect/io/Effect"
import * as NodeFS from "node:fs"
 
export const readFile = (
path: string
): Effect.Effect<never, NodeJS.ErrnoException, Buffer> =>
Effect.async((resume) => {
NodeFS.readFile(path, (error, data) => {
if (error) {
resume(Effect.fail(error))
} else {
resume(Effect.succeed(data))
}
})
})
 
export const readTodos = readFile("todos.txt")
const readTodos: Effect.Effect<never, NodeJS.ErrnoException, Buffer>

What makes all of the above particularly powerful is that we can seamlessly mix synchronous and asynchronous code. When working within the Effect world, everything becomes an Effect.