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 functionconst 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);
[AsyncFiberException: Fiber #0 cannot be resolved synchronously.This is caused by using runSync on an effect that performs asyncwork]
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)
.