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
const divide = (a: number, b: number): number => {
if (b === 0) {
throw new Error("Cannot divide by zero")
}
return a / b
}
ts
const divide = (a: number, b: number): number => {
if (b === 0) {
throw new Error("Cannot divide by zero")
}
return a / 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"
 
const success = Effect.succeed(42)
ts
import { Effect } from "effect"
 
const success = 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"
 
const failure = Effect.fail("my error")
ts
import { Effect } from "effect"
 
const failure = 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"
 
const divide = (a: number, b: number): Effect.Effect<number, Error> =>
b === 0
? Effect.fail(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
ts
import { Effect } from "effect"
 
const divide = (a: number, b: number): Effect.Effect<number, Error> =>
b === 0
? Effect.fail(new Error("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"
 
const log = (message: string) =>
Effect.sync(() => {
console.log(message) // side effect
})
 
const program = log("Hello, World!")
ts
import { Effect } from "effect"
 
const log = (message: string) =>
Effect.sync(() => {
console.log(message) // side effect
})
 
const program = 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.

The thunk passed to 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"
 
const parse = (input: string) =>
Effect.try(
() => JSON.parse(input) // JSON.parse may throw for bad input
)
 
const program = parse("")
ts
import { Effect } from "effect"
 
const parse = (input: string) =>
Effect.try(
() => JSON.parse(input) // JSON.parse may throw for bad input
)
 
const program = 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"
 
const parse = (input: string) =>
Effect.try({
try: () => JSON.parse(input), // JSON.parse may throw for bad input
catch: (unknown) => new Error(`something went wrong ${unknown}`) // remap the error
})
 
const program = parse("")
ts
import { Effect } from "effect"
 
const parse = (input: string) =>
Effect.try({
try: () => JSON.parse(input), // JSON.parse may throw for bad input
catch: (unknown) => new Error(`something went wrong ${unknown}`) // remap the error
})
 
const program = 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 Promises 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

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.

ts
import { Effect } from "effect"
 
const delay = (message: string) =>
Effect.promise<string>(
() =>
new Promise((resolve) => {
setTimeout(() => {
resolve(message)
}, 2000)
})
)
 
const program = delay("Async operation completed successfully!")
ts
import { Effect } from "effect"
 
const delay = (message: string) =>
Effect.promise<string>(
() =>
new Promise((resolve) => {
setTimeout(() => {
resolve(message)
}, 2000)
})
)
 
const program = delay("Async operation completed successfully!")

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)

The Promise within the thunk passed to Effect.promise should never reject.

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.

tryPromise

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.

ts
import { Effect } from "effect"
 
const getTodo = (id: number) =>
Effect.tryPromise(() =>
fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
)
 
const program = getTodo(1)
ts
import { Effect } from "effect"
 
const getTodo = (id: number) =>
Effect.tryPromise(() =>
fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
)
 
const program = getTodo(1)

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:

ts
import { Effect } from "effect"
 
const getTodo = (id: number) =>
Effect.tryPromise({
try: () => fetch(`https://jsonplaceholder.typicode.com/todos/${id}`),
// remap the error
catch: (unknown) => new Error(`something went wrong ${unknown}`)
})
 
const program = getTodo(1)
ts
import { Effect } from "effect"
 
const getTodo = (id: number) =>
Effect.tryPromise({
try: () => fetch(`https://jsonplaceholder.typicode.com/todos/${id}`),
// remap the error
catch: (unknown) => new Error(`something went wrong ${unknown}`)
})
 
const program = 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 * as NodeFS from "node:fs"
 
const readFile = (filename: string) =>
Effect.async<Buffer, Error>((resume) => {
NodeFS.readFile(filename, (error, data) => {
if (error) {
resume(Effect.fail(error))
} else {
resume(Effect.succeed(data))
}
})
})
 
const program = readFile("todos.txt")
ts
import { Effect } from "effect"
import * as NodeFS from "node:fs"
 
const readFile = (filename: string) =>
Effect.async<Buffer, Error>((resume) => {
NodeFS.readFile(filename, (error, data) => {
if (error) {
resume(Effect.fail(error))
} else {
resume(Effect.succeed(data))
}
})
})
 
const program = 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:

  1. 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.

    ts
    import { Effect } from "effect"
     
    let i = 0
     
    const bad = Effect.succeed(i++)
     
    const good = Effect.suspend(() => Effect.succeed(i++))
     
    console.log(Effect.runSync(bad)) // Output: 0
    console.log(Effect.runSync(bad)) // Output: 0
     
    console.log(Effect.runSync(good)) // Output: 1
    console.log(Effect.runSync(good)) // Output: 2
    ts
    import { Effect } from "effect"
     
    let i = 0
     
    const bad = Effect.succeed(i++)
     
    const good = Effect.suspend(() => Effect.succeed(i++))
     
    console.log(Effect.runSync(bad)) // Output: 0
    console.log(Effect.runSync(bad)) // Output: 0
     
    console.log(Effect.runSync(good)) // Output: 1
    console.log(Effect.runSync(good)) // Output: 2

    In 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).

  2. 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 for Effect.suspend to be used in recursive functions to escape an eager call. For instance:

    ts
    import { Effect } from "effect"
     
    const blowsUp = (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 memory
     
    const allGood = (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
    ts
    import { Effect } from "effect"
     
    const blowsUp = (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 memory
     
    const allGood = (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
  3. 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:

    ts
    import { Effect } from "effect"
     
    const ugly = (a: number, b: number) =>
    b === 0
    ? Effect.fail(new Error("Cannot divide by zero"))
    : Effect.succeed(a / b)
     
    const nice = (a: number, b: number) =>
    Effect.suspend(() =>
    b === 0
    ? Effect.fail(new Error("Cannot divide by zero"))
    : Effect.succeed(a / b)
    )
    ts
    import { Effect } from "effect"
     
    const ugly = (a: number, b: number) =>
    b === 0
    ? Effect.fail(new Error("Cannot divide by zero"))
    : Effect.succeed(a / b)
     
    const nice = (a: number, b: number) =>
    Effect.suspend(() =>
    b === 0
    ? Effect.fail(new Error("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.

FunctionGivenTo
succeedAEffect<A>
failEEffect<never, E>
sync() => AEffect<A>
try() => AEffect<A, UnknownException>
try (overload)() => A, unknown => EEffect<A, E>
promise() => Promise<A>Effect<A>
tryPromise() => Promise<A>Effect<A, UnknownException>
tryPromise (overload)() => Promise<A>, unknown => EEffect<A, E>
async(Effect<A, E> => void) => voidEffect<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.