Getting Started #

What an Effect is, how to read its type, how to build one, and how to run it.

The problem. A plain TypeScript function hides 3 facts from its caller. Look at this signature:

async function loadUser(id: string): Promise<User>

The signature says that the function returns a User. It does not say that the function can throw NotFound or NetworkError. It does not say that the function needs a database connection and a logger. Also, the function starts to run when you call it. Thus you cannot retry it, apply a timeout to it, or run 2 of them in parallel without extra code. The failure, the requirements, and the start of the work are all hidden.

The shift

Today you think of a function as code that does work when you call it. In Effect, a program is a value that describes work. The value does not do the work. You build the value, you combine it with other values, and at the end you give it to the runtime. The runtime does the work.

Because an effect is a value, its type can show all the facts that the plain function hid:

Effect<Success, Error, Requirements>
Effect<User,    NotFound | NetworkError, Database | Logger>

Read the type as follows: "When you run this effect, it gives a User, or it fails with one of these errors, and it needs these services." The compiler checks all 3 parts. If you do not catch an error, the error stays in the type. If you do not provide a service, the program does not compile.

This design has a benefit in all other sections of this course. Retries, timeouts, concurrency, resource cleanup, and tests all become plain functions. Each function takes an effect and returns a new effect, because an effect is data.

Promise Effect
Starts to run Immediately when you create it Only when you call a run function
Error type any, not visible The second type parameter holds it
Dependencies Hidden inside closures The third type parameter holds them
Reusable No, a Promise resolves 1 time Yes, you can run the same effect many times
Cancel Not built in Interrupt is built in

In this section you learn to create effects, read their types, put them in sequence, and run them. Nothing more.

Learn #

Lesson 1. An Effect is a description, not an action #

The most important idea comes first. When you create an effect, nothing happens. In plain TypeScript this line prints immediately:

const p = new Promise<void>((resolve) => { console.log("hi"); resolve() })
// "hi" is already on the screen

The Effect version below builds a description of "print hi" and puts it in a variable. Nothing prints until you call a run function. Because the effect is only a description, you can run it many times. Each run does the work again.

import { Effect } from "effect"

// This builds a description. Nothing prints yet.
const sayHi = Effect.sync(() => console.log("hi"))

console.log("before running")

// Running the description does the work. Running twice does it twice.
Effect.runSync(sayHi)
Effect.runSync(sayHi)

console.log("after running")
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).
Notice

Notice that "before running" prints first, although the code created sayHi before it. Delete both runSync lines: "hi" does not print. The description does not do the work. The run function does the work.

Lesson 2. Reading the type: Effect<A, E, R> #

Every effect has 3 type parameters. If you hover over the variables in a real editor, you see these types:

Constructor Type Meaning
Effect.succeed(42) Effect<number, never, never> Gives a number, cannot fail, needs no services
Effect.fail("nope") Effect<never, string, never> Gives no value, fails with a string, needs no services

never is the TypeScript type for "this cannot happen". When E is never, the compiler guarantees that the effect cannot fail. When R is never, the effect needs no services.

To see both outcomes without a crash, run the effect with Effect.runSyncExit. This function returns an Exit. An Exit is a plain value. It is a Success with a value, or a Failure with a cause. Later sections explain Exit and Cause in detail. For now, only the tag is important.

import { Effect, Exit } from "effect"

const ok = Effect.succeed(42)              // Effect<number, never, never>
const bad = Effect.fail("no permission")   // Effect<never, string, never>

const exit1 = Effect.runSyncExit(ok)
const exit2 = Effect.runSyncExit(bad)

console.log(exit1._tag, Exit.isSuccess(exit1) ? exit1.value : "")
console.log(exit2._tag, Exit.isFailure(exit2) ? "it failed" : "")
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).
Notice

Run bad with Effect.runSync in place of runSyncExit. The process throws, because runSync expects success. runSyncExit never throws.

Lesson 3. Sequencing steps with Effect.gen #

Most real programs have several steps in sequence. Plain TypeScript uses async/await:

async function main() {
  const user = await loadUser("1")
  const posts = await loadPosts(user.id)
  return posts.length
}

Effect uses a generator function with yield* in the same positions. Effect.gen takes a generator and gives one effect. This effect runs the steps in order. Where you write await in plain TypeScript, you write yield*. Where you write return, you still write return. The returned value becomes the success value.

The whole generator is still only a description. Nothing inside it runs until you run the outer effect.

import { Effect } from "effect"

// Two tiny "services" that just return data
const loadUser = (id: string) => Effect.succeed({ id, name: "Ada" })
const loadPosts = (userId: string) => Effect.succeed(["post-1", "post-2", "post-3"])

// Same shape as async/await, with yield* instead of await
const program = Effect.gen(function* () {
  const user = yield* loadUser("1")
  const posts = yield* loadPosts(user.id)
  console.log(user.name, "has", posts.length, "posts")
  return posts.length
})

const count = Effect.runSync(program)
console.log("returned:", count)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).
Notice

The return inside the generator became the value that runSync gave back. Remove one yield*: TypeScript reports an error, because the variable now holds an effect and not a user.

Lesson 4. Transforming with pipe: map, tap, andThen #

Effect.gen is for step-by-step logic. For small transformations, Effect also gives a pipeline style. This style is similar to array methods, but for effects.

Function What it does Plain TS equivalent
Effect.map(f) Changes the success value promise.then((x) => f(x)) with a plain function
Effect.tap(f) Runs a side effect and keeps the value A log call inside .then that returns x again
Effect.andThen(f) Continues with the next effect promise.then((x) => otherPromise(x))

Read a pipe from top to bottom. The value on the left goes into each function in turn. Each step returns a new effect. Thus the pipeline is still a description until you run it.

import { Effect } from "effect"

const getPrice = Effect.succeed(100)

const program = getPrice.pipe(
  Effect.map((price) => price * 1.15),                        // add tax
  Effect.tap((total) => Effect.sync(() => console.log("total is", total))),
  Effect.andThen((total) => Effect.succeed("charged " + total)) // chain into another Effect
)

console.log(Effect.runSync(program))
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).
Notice

tap did not change the value. It only read the value. If you replace tap with map, the pipeline carries undefined forward, because console.log returns nothing. This is the difference between the 2 functions.

Lesson 5. Control flow: forEach, all, and when #

Real programs run one effect for each element of a list, or several effects together. In plain TypeScript you write a loop and Promise.all:

const names = []
for (const id of [1, 2, 3]) names.push(await loadName(id))
const [name, posts] = await Promise.all([loadName(1), countPosts(1)])
if (await isAdmin(1)) await notify("admin Ada")

Effect has one function for each of these shapes. Each function takes effects and returns one new effect. The result type follows the shape of the input.

Function Input Result type Use it when
Effect.forEach(items, f) A list and a function that returns an effect Effect<Array<B>> You need one result per element
Effect.forEach(items, f, { discard: true }) The same Effect<void> You only need the side effects
Effect.all([a, b]) A tuple of effects Effect<[A, B]> A fixed number of different effects
Effect.all({ a, b }) An object of effects Effect<{ a: A; b: B }> You want the results by name
Effect.when(effect, condition) An effect and an Effect<boolean> Effect<Option<A>> The condition is itself an effect

forEach and all run the effects in sequence by default. The Concurrency section shows the concurrency option. when returns an Option: Some with the value when the effect ran, None when it did not run. An Option is the Effect data type for "a value or nothing".

Note: Effect v4 has no Effect.unless and no Effect.if. For a plain boolean, write an if statement inside Effect.gen.

import { Effect, Option } from "effect"

const names: Record<number, string> = { 1: "Ada", 2: "Lin", 3: "Bo" }
const loadName = (id: number) => Effect.succeed(names[id] ?? "?")
const countPosts = (id: number) => Effect.succeed(id * 2)
const notify = (name: string) => Effect.sync(() => console.log("notified", name))
const isAdmin = (id: number) => Effect.succeed(id === 1)

const program = Effect.gen(function* () {
  // 1. forEach: one effect per element, in order. The results come back as an array.
  const loaded = yield* Effect.forEach([1, 2, 3], loadName)          // Array<string>
  console.log("names:", loaded.join(", "))

  // 2. forEach with discard: only the side effects. The result is void.
  yield* Effect.forEach(loaded, notify, { discard: true })

  // 3. all over a tuple: the result is a typed tuple.
  const [name, posts] = yield* Effect.all([loadName(1), countPosts(1)])   // [string, number]
  console.log(name, "has", posts, "posts")

  // 4. all over an object: the result has the same keys as the input.
  const data = yield* Effect.all({ name: loadName(2), posts: countPosts(2) })   // { name: string; posts: number }
  console.log(data.name, "has", data.posts, "posts")

  // 5. when: the effect runs only if the condition effect gives true. The result is an Option.
  const sent = yield* Effect.when(notify("admin Ada"), isAdmin(1))
  const skipped = yield* Effect.when(notify("admin Lin"), isAdmin(2))
  console.log("sent:", Option.isSome(sent), "skipped:", Option.isNone(skipped))
})

Effect.runSync(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).
Notice

Change data.posts to data.post. TypeScript reports an error, because the result of Effect.all has only the keys of the input object. Remove { discard: true } from step 2. The output is the same, but the yield* now gives an array of 3 undefined values.

Lesson 6. Loops and recursion: whileLoop and suspend #

Some work does not have a list to loop over. A countdown runs until a number is 0. A poll runs until a job is complete. There are 3 ways to write this with effects:

  1. A plain for or while loop inside Effect.gen. Each step has yield*.
  2. Effect.whileLoop({ while, body, step }). while is a function that returns a boolean. body is a function that returns the effect for one step. step receives the result of each step.
  3. A function that returns an effect and calls itself.

The third way has a trap. Look at the function eagerSumTo in the code. When you call eagerSumTo(5), the function calls eagerSumTo(4) at once, and so on to 0. Thus the call builds all 6 effects before any run. For a large number, the call uses too much stack space and throws a RangeError.

Effect.suspend(() => effect) is the fix. It takes a function and delays the call until the effect runs. Thus lazySumTo(5) builds nothing. The runtime calls the function one step at a time, and the stack stays small.

Note: Effect v4 has no Effect.loop and no Effect.iterate. Use the 3 ways above.

import { Effect } from "effect"

// 1. A plain loop inside Effect.gen. yield* runs each step.
const countdown = Effect.gen(function* () {
  for (let n = 3; n > 0; n--) {
    yield* Effect.sync(() => console.log("t-minus", n))
  }
  console.log("liftoff")
})

// 2. whileLoop: while and body are functions, so both run at run time, not before.
let remaining = 3
const drain = Effect.whileLoop({
  while: () => remaining > 0,
  body: () => Effect.sync(() => remaining--),
  step: (left) => console.log("drained, before:", left)
})

// 3. Recursion without suspend. The call eagerSumTo(5) builds the whole chain before any run.
let built = 0
const eagerSumTo = (n: number): Effect.Effect<number> => {
  built++
  return n === 0 ? Effect.succeed(0) : Effect.map(eagerSumTo(n - 1), (rest) => rest + n)
}
const eager = eagerSumTo(5)
console.log("eager: built", built, "effects before run")

// 4. suspend delays the body until the effect runs. The call lazySumTo(5) builds nothing.
built = 0
const lazySumTo = (n: number): Effect.Effect<number> =>
  Effect.suspend(() => {
    built++
    return n === 0 ? Effect.succeed(0) : Effect.map(lazySumTo(n - 1), (rest) => rest + n)
  })
const lazy = lazySumTo(5)
console.log("lazy: built", built, "effects before run")

Effect.runSync(countdown)
Effect.runSync(drain)
console.log("eager sum:", Effect.runSync(eager))
console.log("lazy sum:", Effect.runSync(lazy), "- built", built, "during the run")
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).
Notice

Change eagerSumTo(5) to eagerSumTo(200000). The call throws a RangeError before any run, because each nested call needs stack space. Change lazySumTo(5) to lazySumTo(200000) in place of this. The program prints the sum, because the runtime builds one step at a time. Caution: a function that returns an effect and calls itself must make the call inside Effect.suspend or inside Effect.gen.

Lesson 7. Bringing in code that can throw or is async #

Real code calls JSON.parse, fetch, and other functions that throw or return Promises. Effect gives one constructor for each situation. When you select the correct constructor, the error becomes visible in the type.

Your code is... Use Error type becomes
Sync, cannot throw Effect.sync(() => ...) never
Sync, can throw Effect.try({ try, catch }) The return type of catch
Async, cannot reject Effect.promise(() => ...) never
Async, can reject Effect.tryPromise({ try, catch }) The return type of catch

The catch function receives the thrown value as unknown. You change this value into a typed value. In this example, both errors become plain strings. In the Error Management section, you use tagged error classes in place of strings.

import { Effect } from "effect"

// A sync operation that can throw, made safe and typed
const parseJson = (raw: string) =>
  Effect.try({
    try: () => JSON.parse(raw) as { port: number },
    catch: () => "invalid json"          // Effect<{ port: number }, string>
  })

// A fake async API that rejects for unknown ids
const fakeFetch = (id: number): Promise<string> =>
  id === 1 ? Promise.resolve("Ada") : Promise.reject(new Error("404"))

const fetchName = (id: number) =>
  Effect.tryPromise({
    try: () => fakeFetch(id),
    catch: () => "user " + id + " not found"   // Effect<string, string>
  })

const program = Effect.gen(function* () {
  const config = yield* parseJson('{"port": 8080}')
  const name = yield* fetchName(1)
  console.log("port", config.port, "user", name)
})

Effect.runPromise(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).
Notice

Change fetchName(1) to fetchName(2). The program fails with the string "user 2 not found", and runPromise rejects. This is not an unexpected crash. The type described this failure from the start.

Lesson 8. Running: runSync, runPromise, and the Exit variants #

An effect runs only at the edge of your program, when you give it to a runner. Select the runner with 2 questions. Is the work synchronous? Do you want a failure to throw?

Runner Async permitted? On failure
Effect.runSync No, it throws when it finds async work Throws
Effect.runSyncExit No Returns Exit.Failure
Effect.runPromise Yes Rejects the Promise
Effect.runPromiseExit Yes Resolves with Exit.Failure

Keep one run call at the top of your application. All code below this call stays a description, and you can combine it with other descriptions. Note: many small runSync calls in the code are a sign that the code does not use effects as values.

import { Effect, Exit } from "effect"

const syncWork = Effect.succeed("sync result")
const asyncWork = Effect.promise(() => new Promise<string>((resolve) => setTimeout(() => resolve("async result"), 10)))
const failing = Effect.fail("boom")

const main = async () => {
  console.log(Effect.runSync(syncWork))
  console.log(await Effect.runPromise(asyncWork))

  const exit = await Effect.runPromiseExit(failing)
  console.log(Exit.isFailure(exit) ? "handled failure without throwing" : "unexpected")

  try {
    Effect.runSync(asyncWork)   // async inside runSync is not allowed
  } catch (e) {
    console.log("runSync refused async work")
  }
}

main()
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).
Notice

The last case is a common mistake on the first day. If an effect contains an async step, runSync throws. If you are not sure, use runPromise.

Do and don't #

DoDon'tWhy
Give every effect to one runner at the top of the application.Do not call Effect.runSync inside functions that build effects.An effect that runs early is no longer a value, so you cannot combine it, retry it, or test it.
Use Effect.try for synchronous code that can throw.Do not put code that can throw inside Effect.sync.A throw inside Effect.sync becomes a defect with no type, and Effect.catch cannot see it.
Use Effect.tryPromise for a Promise that can reject.Do not wrap a Promise that can reject in Effect.promise.Effect.promise declares that the Promise cannot reject, so a rejection becomes a defect and E stays never.
Write yield* in front of every effect inside Effect.gen.Do not assign an effect to a variable and then use it as a plain value.Without yield*, the variable holds the effect itself, and the code does not compile or prints an object.
Use Effect.andThen (or Effect.flatMap) when the function returns an effect.Do not use Effect.map with a function that returns an effect.Effect.map wraps the return value, so you get an effect inside an effect and the next step sees an object.
Use Effect.runPromise when the effect contains async work.Do not use Effect.runSync on an effect that waits for a Promise or a timer.Effect.runSync must complete before it returns, so it throws when it finds async work.
Declare the error type in the return annotation, for example Effect.Effect<number, string>.Do not annotate a function as Effect.Effect<number> when its body calls Effect.fail.Effect.Effect<number> means E is never, so the compiler rejects the Effect.fail in the body.
Put the body of a function that returns an effect and calls itself inside Effect.suspend.Do not let such a function call itself directly in a map or andThen argument.The first call builds the whole chain before any run, and a deep chain throws a RangeError.

Fix it #

Each program below is broken or incomplete. Make it print the expected output with zero type errors. Use hints before the solution.

1. The forgotten yield* #

This program must print 2, but it does not compile. Correct it. Do not change the console.log line.

expected output: 2
import { Effect } from "effect"

const program = Effect.gen(function* () {
  const n = Effect.succeed(1)
  console.log(n + 1)
})

Effect.runSync(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

2. Nothing happens #

The program is correct, but it prints nothing. Make it print hello from effect.

expected output: hello from effect
import { Effect } from "effect"

const program = Effect.sync(() => console.log("hello from effect"))

program
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

3. The loop that runs nothing #

The program must greet both names before it prints done, but only done prints. Replace the for loop with one Effect function that runs greet for each name. Do not change greet.

expected output: hello Ada hello Lin done
import { Effect } from "effect"

const greet = (name: string) => Effect.sync(() => console.log("hello", name))

const program = Effect.gen(function* () {
  for (const name of ["Ada", "Lin"]) {
    greet(name)
  }
  console.log("done")
})

Effect.runSync(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

4. The key that does not exist #

The program does not compile. The code reads a property that the result of Effect.all does not have. Correct the property name so that the program prints Ada has 2 posts. Do not change the object that goes into Effect.all.

expected output: Ada has 2 posts
import { Effect } from "effect"

const loadName = (id: number) => Effect.succeed(id === 1 ? "Ada" : "Lin")
const countPosts = (id: number) => Effect.succeed(id * 2)

const program = Effect.gen(function* () {
  const data = yield* Effect.all({ name: loadName(1), posts: countPosts(1) })
  console.log(data.name, "has", data.postCount, "posts")
})

Effect.runSync(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

5. A throw in the wrong constructor #

JSON.parse throws on bad input. At the moment, Effect treats this throw as a bug (a defect), and the raw SyntaxError comes out. Change only the constructor for the parse step. The failure must become the typed error "invalid json", and the program must print error: invalid json.

expected output: error: invalid json
import { Cause, Effect, Exit } from "effect"

const parse = (raw: string) =>
  Effect.sync(() => JSON.parse(raw) as { name: string })

const exit = Effect.runSyncExit(parse("{ not json"))

if (Exit.isFailure(exit)) {
  console.log("error:", Cause.squash(exit.cause))
} else {
  console.log("parsed", exit.value.name)
}
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

6. Wrong runner #

The program contains async work, and it crashes when it runs. Change the runner so that the program prints done after 5ms.

expected output: done after 5ms
import { Effect } from "effect"

const wait = Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, 5)))

const program = Effect.gen(function* () {
  yield* wait
  console.log("done after 5ms")
})

Effect.runSync(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

7. map or andThen? #

The pipeline must double the number and print 20, but it prints an unexpected value. Correct the pipeline so that the doubled value is a plain number.

expected output: 20
import { Effect } from "effect"

const double = (n: number) => Effect.succeed(n * 2)

const program = Effect.succeed(10).pipe(
  Effect.map((n) => double(n)),
  Effect.map((result) => console.log(String(result)))
)

Effect.runSync(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

8. The type annotation that lies #

parseAge has a return type that says it cannot fail, but its body can fail. Correct the return type annotation so that the code compiles and prints the 2 lines below. Do not remove the Effect.fail.

expected output: age 30 rejected abc
import { Effect, Exit } from "effect"

const parseAge = (input: string): Effect.Effect<number> => {
  const n = Number(input)
  return Number.isNaN(n) ? Effect.fail("not a number: " + input) : Effect.succeed(n)
}

for (const input of ["30", "abc"]) {
  const exit = Effect.runSyncExit(parseAge(input))
  console.log(Exit.isSuccess(exit) ? "age " + exit.value : "rejected " + input)
}
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

Build it #

Write the program from the spec. The output must match exactly.

1. Safe division #

Write divide(a, b) that returns an effect. The effect fails with the string "division by zero" when b is 0. Otherwise it succeeds with a / b. Then loop over the pairs [10, 2], [7, 0], [9, 3]. Run each pair with Effect.runSyncExit and print one line per pair in this exact format:

10 / 2 = 5
7 / 0 failed: division by zero
9 / 3 = 3

To read the error out of a failed Exit, use Cause.squash(exit.cause). This function returns the failure value.

expected output: 10 / 2 = 5 7 / 0 failed: division by zero 9 / 3 = 3
import { Cause, Effect, Exit } from "effect"

// TODO: divide returns Effect<number, string>
const divide = (a: number, b: number) => {
  throw new Error("TODO")
}

const pairs: Array<[number, number]> = [[10, 2], [7, 0], [9, 3]]

for (const [a, b] of pairs) {
  // TODO: run divide(a, b) with Effect.runSyncExit and print the result
}
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

2. Config loader pipeline #

You have a raw config string, and you need a validated port number. Build 3 small effects and one Effect.gen that combines them:

  1. parse(raw) uses Effect.try to parse JSON into { port: unknown }. It fails with "bad json".
  2. toNumber(value) succeeds with the value if the value is a number. Otherwise it fails with "port is not a number".
  3. inRange(port) succeeds if 1 <= port <= 65535. Otherwise it fails with "port out of range".

loadPort(raw) combines the 3 steps with Effect.gen and returns the port. Run it on the 3 inputs below with Effect.runSyncExit and print exactly:

ok 8080
error port out of range
error bad json

Inputs, in order: '{"port": 8080}', '{"port": 70000}', 'not json'.

expected output: ok 8080 error port out of range error bad json
import { Cause, Effect, Exit } from "effect"

// TODO: parse, toNumber, inRange

const loadPort = (raw: string) =>
  Effect.gen(function* () {
    // TODO: chain the three steps and return the port
    return 0
  })

const inputs = ['{"port": 8080}', '{"port": 70000}', "not json"]

for (const raw of inputs) {
  const exit = Effect.runSyncExit(loadPort(raw))
  // TODO: print "ok <port>" or "error <message>"
}
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

3. Fake API client #

Wrap a Promise-based API in Effect. The API is given: api.getUser(id) resolves with a user for the ids 1 and 2, and rejects for other ids.

Write getUser(id) with Effect.tryPromise. A rejection must become the typed error "user <id> not found". Then write program with Effect.gen. The program gets users 1 and 2 and prints their names on one line as Ada & Lin. Then it tries user 3 and prints the error. Get user 3 with Effect.runPromiseExit and use Cause.squash to get the message. Exact output:

Ada & Lin
user 3 not found
expected output: Ada & Lin user 3 not found
import { Cause, Effect, Exit } from "effect"

const api = {
  getUser: (id: number): Promise<{ id: number; name: string }> => {
    const users: Record<number, string> = { 1: "Ada", 2: "Lin" }
    return id in users ? Promise.resolve({ id, name: users[id]! }) : Promise.reject(new Error("404"))
  }
}

// TODO: getUser(id) with Effect.tryPromise

const program = Effect.gen(function* () {
  // TODO: fetch users 1 and 2, print "Ada & Lin"
  // TODO: run getUser(3) via Effect.runPromiseExit and print the error message
})

Effect.runPromise(program)
⌘/Ctrl + Enter
Press Run (or ⌘/Ctrl+Enter in the editor).

Recall #

Answer in your head first, then reveal. Come back to these tomorrow.

What happens when you create an effect but do not run it? #

Nothing. An effect is a description of work. Only a runner such as runSync or runPromise does the work. A Promise is different: it starts the work when you create it.

What is the type of `Effect.fail(new Error("x"))`? #

Effect<never, Error, never>. It gives no success value, it fails with an Error, and it needs no services.

In `Effect<A, E, R>`, what does `R = never` mean? #

The effect needs no services from outside to run. In the Requirements Management section, R becomes a type other than never.

Which constructor do you use to wrap `fs.readFileSync`, which can throw? #

Effect.try({ try, catch }). The function is synchronous and can throw. Effect.sync is only for code that cannot throw. A throw inside Effect.sync becomes a defect, not a typed failure.

What is the difference between `Effect.map` and `Effect.andThen`? #

map takes a plain function and wraps its return value. andThen (or flatMap) takes a function that returns an effect and removes the nested layer. Thus you do not get an effect inside an effect.

When does `Effect.runSync` throw? #

It throws when the effect fails, or when the effect contains async work such as a Promise or a timer. Use runSyncExit to get a value in place of a throw. Use runPromise when the effect has async work.

Inside `Effect.gen`, what is the equivalent of `await`? #

yield*. It unwraps the success value of an effect. If the effect fails, it stops the generator.

What is the success type of `Effect.all({ user: loadUser(1), posts: loadPosts(1) })`, where `loadUser` gives a `User` and `loadPosts` gives an `Array<Post>`? #

{ user: User; posts: Array<Post> }. Effect.all keeps the keys of the input object. With a tuple input, it gives a tuple. With Effect.forEach over a list, you get an Array of the results.