Effect.promise

In the previous lesson we saw how Effect.runPromise allows us to execute Effects asynchronously.

Let’s take a look at how to construct an Effect with asynchronous contents.

import { Effect } from "effect";
// a classic asynchronous function
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const program = Effect.promise(() => sleep(1000));
console.log("starting at", new Date())
program.pipe(
Effect.map(() => console.log("finishing at", new Date()),
Effect.runPromise
);

The signature for Effect.promise is very similar to Effect.sync (3) , except that it takes an asynchronous thunk as the argument.

We can see the difference by calling the two functions:

function doSyncWork() {
return 3;
}
async function doAsyncWork() {
return 3;
}
const syncProgram: Effect.Effect<number> = Effect.sync(doSyncWork);
const asyncProgram: Effect.Effect<number> = Effect.promise(doAsyncWork);
const nestedProgram: Effect.Effect<Promise<number>> = Effect.sync(doAsyncWork);
console.log("SYNC EFFECT:", syncProgram);
console.log("ASYNC EFFECT:", asyncProgram);
console.log("NESTED EFFECT:", nestedProgram);

Looking at the types we cannot tell the difference between syncProgram and asyncProgram. The internal structure of the tree is different, but this is an implementation detail. They are both “an Effect of number”. If we passed both programs to Effect.runPromise (10) , they would behave the same way.

Although you can run a synchronous Effect using Effect.runPromise (10) , you cannot run an asynchronous Effect using Effect.runSync (2) .

Effect.promise(async () => 3).pipe(Effect.runSync);
Terminal window
[AsyncFiberException: Fiber #0 cannot be resolved synchronously.
This is caused by using runSync on an effect that performs async
work]

The message might seem cryptic - we haven’t introduced the concept of “Fibers” yet - but this should not be too surprising. This asymmetry matches the behaviour of standard JavaScript: you can call a synchronous function inside an asynchronous function, but you cannot await inside a synchronous function.

This lack of type safety might seem surprising, but it turns out not to be a big problem in practice. In JavaScript, if any part of your code is asynchronous, then the overall main program will have to run asynchronously. It’s rare to have a whole JavaScript program that is entirely synchronous, and most programs will need Effect.runPromise (10) rather than Effect.runSync (2) .

You can chain multiple pieces of async work together, including a mix of sync and async, and it behaves in the way you would expect:

const print = (message: string) => console.log(new Date(), message);
const delay = (message: string) =>
Effect.promise(() => sleep(1000)).pipe(
Effect.map(() => print("debug: slept for 1000ms")),
Effect.map(() => message),
);
print("starting");
delay("one").pipe(
Effect.map((one) => print(`${one} after 1 second`)),
Effect.flatMap(() => delay("two")),
Effect.map((two) => two.toUpperCase()),
Effect.flatMap((two) => delay(`${two} three`)),
Effect.map((twoThree) => print(`${twoThree} after 3 seconds`)),
Effect.runPromise,
);

You can do more practical examples:

const program = Effect.promise(() => fetch("https://httpbin.org/uuid")).pipe(
Effect.flatMap((response) => Effect.promise(() => response.json())),
Effect.map(console.log),
);
Effect.runPromise(program);

However, it is important to note that Effect.promise returns Effect.Effect<A, never>. In other words it is designed for asynchronous work that will never fail. Most asynchronous work - HTTP requests, database calls, file system access, waiting for user input - can fail. Thankfully, just like how Effect.try (9) can be more useful than Effect.sync (3) , there’s a failure-aware alternative to Effect.promise. In the next lesson we’ll meet Effect.tryPromise (12) .