Effect v4Module 2 of 8: Sequence operations
Module 2 of 8Composition modelsrc/course/examples/sequence.ts

One operation can feed the next

Use one Effect result as the input to the next operation without starting either step during assembly.

First principle

A multi-step operation is a sequence of values and actions. Each step must finish before the next step receives its input.

02 / 08module position
01

The concrete friction

Sequencing Promises forces immediate execution of the first step

Computation B often depends on the output of computation A (e.g. loading an issue requires the issue ID found by search). In vanilla async/await, writing const id = await findId(); return load(id); immediately executes findId(). The two steps cannot be combined into a single inert pipeline value that can be handed to a retry schedule, concurrency throttler, or scoped boundary.

First-principles consequence

Causality requires step A to complete before step B begins. But coupling sequencing to immediate execution destroys composition: you cannot retry the sequence (A -> B) as an atomic unit or pass the un-started workflow across architectural layers.

baseline-friction.tsbaseline problem
const findIssueId = async (): Promise<string> => "ISSUE-101";
const loadIssue = async (issueId: string) => ({
  id: issueId,
  title: "Add typed search errors"
});

const issueIdPromise = findIssueId();
const issuePromise = issueIdPromise.then((issueId) => loadIssue(issueId));
const issue = await issuePromise;
02

The mental model

Generator coroutines suspend execution algebraically

Effect.gen uses JavaScript generator syntax (function* and yield*) to express data dependency without eager execution. The expression yield* findIssueId suspends the coroutine until findIssueId produces its success value (A). The runtime then resumes the generator, binding the value to issueId and passing it to loadIssue(issueId). The entire generator block remains a single un-evaluated Effect value until run.

Assemble one program:
   const program = Effect.gen(function* () { ... })
   No step has run yet.

Execute the program:
   findIssueId ──> "ISSUE-101"
                    │
                    ▼
                loadIssue("ISSUE-101") ──> Issue

The second step receives the first step's value.
03

Minimal working example

Executable source

This is the checked source file used by the course. Read it before you run it.

src/course/examples/sequence.tsComposition model
import { Console, Effect } from "effect"

export type Issue = {
  readonly id: string
  readonly title: string
}

const findIssueId = Effect.sync(() => "ISSUE-101")

const loadIssue = (issueId: string) =>
  Effect.sync<Issue>(() => ({
    id: issueId,
    title: "Add typed search errors"
  }))

const program = Effect.gen(function* () {
  const issueId = yield* findIssueId
  const issue = yield* loadIssue(issueId)
  return issue
})

await Effect.runPromise(
  Effect.gen(function* () {
    yield* Console.log("The program is assembled before either step runs")
    const issue = yield* program
    yield* Console.log(`loaded ${issue.id}: ${issue.title}`)
  })
)
04

Execute and verify

Predict the result, run the command, and compare the output with the model.

Run this command
pnpm exec tsx src/course/examples/sequence.ts
Before you run it

Which value does loadIssue receive, and when does it receive it? Why is the second operation defined as a function of issueId?

Reveal expected output
The program is assembled before either step runs
loaded ISSUE-101: Add typed search errors
05

Error anatomy and edge cases

Passing an Effect blueprint where a raw value is expected

Calling loadIssue(findIssueId) without yielding findIssueId passes the Effect blueprint itself rather than the resolved issue ID string.

Compiler or runtime diagnostic

TypeScript reports: "TS2345: Argument of type 'Effect<string, never, never>' is not assignable to parameter of type 'string'". If yield* is called outside of an Effect.gen generator function, TypeScript reports: "TS1160: The 'yield' expression is only allowed within the body of a generator function".

Remedy

Use const issueId = yield* findIssueId inside Effect.gen to extract the success value from the Effect before passing it to loadIssue(issueId).

06

Active lab exercise

src/exercises/exercise2/starter.ts

Exercise: sequence dependent issue operations

Pass the issue ID produced by the first operation to the second operation.

ObjectiveFix starter.ts so loadIssue receives ISSUE-101 from findIssueId instead of a hard-coded ID.
pnpm exec tsx src/exercises/exercise2/exercise.test.ts starter

The harness should fail against the starter. Inspect the assertion, change the starter implementation, and run it again.

Inspect starter code
Broken starter implementation
src/exercises/exercise2/starter.tsstarter
import { Effect } from "effect"

type Issue = {
  readonly id: string
  readonly title: string
}

const findIssueId = Effect.succeed("ISSUE-101")

const loadIssue = (issueId: string) =>
  Effect.succeed<Issue>({
    id: issueId,
    title: "Add typed search errors"
  })

export const loadIssueAfterSearch = Effect.gen(function* () {
  yield* findIssueId
  return yield* loadIssue("ISSUE-999")
})
Reveal reference solution
Target reference solution
solution.tsverified solution
import { Effect } from "effect"

type Issue = {
  readonly id: string
  readonly title: string
}

const findIssueId = Effect.succeed("ISSUE-101")

const loadIssue = (issueId: string) =>
  Effect.succeed<Issue>({
    id: issueId,
    title: "Add typed search errors"
  })

export const loadIssueAfterSearch = Effect.gen(function* () {
  const issueId = yield* findIssueId
  return yield* loadIssue(issueId)
})
Why this solution works

Bind the result with const issueId = yield* findIssueId, then pass issueId to loadIssue. The second Effect is created only after the first Effect produces its value.