Effect · lesson 1
Lesson 1 · the type
Description, not a Promise
Until you can see that an Effect has not run yet, every later feature (errors, Layers, fibers) will look like extra syntax for async.
You asked to learn Effect so typed failures, retries, and dependencies can live in TypeScript instead of in comments.
The library’s first move is not a better Promise. It is a value that describes a program.
The docs put it bluntly: creating an Effect does not execute it.
Three channels, zero work
Effect<A, E, R>
What you get if the workflow finishes well. Effect.succeed(42) is Effect<number, never, never>.
Expected failure, as a value — not a hidden throw. never means that channel is empty.
Services the runtime must supply. never means you can run it without providing anything.
Ordinary TypeScript only tracks success.
Why Effect? rewrites divide so “cannot divide by zero” is in the type:
Effect<number, Error, never>.
We will fail on purpose in a later lesson. Today: the value is still just a plan.
Eager vs lazy
Effect vs Promise:
a Promise is eager and one-shot.
An Effect is lazy and repeatable.
new Promise(fn) calls fn now.
Effect.sync(fn) stores fn. The thunk runs each time a run* function interprets the value
(Creating Effects).
Lab: when does the world move?
This is a model of the documented behavior, not the Effect runtime. Construct, then run. Watch the world log.
Run at the edge
Running Effects wants most of the program to stay as Effects.
Interpretation happens once, at the rim: a CLI main, a test, a React click.
Prefer runPromise or runFork.
runSync is the exception — sync, and you accept a throw on failure or on accidental async.
import { Effect } from "effect"
const program = Effect.sync(() => {
console.log("Hello, World!")
return 1
})
const result = Effect.runSync(program)
// log happens here, not at Effect.sync
The same idea in React
Official Vite + React install does not put Effect inside render. It memoizes the description and runs it on click. React still owns the count.
const task = useMemo(
() => Effect.sync(() => setCount((current) => current + 1)),
[setCount]
)
const increment = useCallback(() => Effect.runSync(task), [task])
Retrieval
Answers are the same length on purpose. Pick from memory, then check.
An Effect value, by itself, is
The docs call it a description of a workflow. Nothing has been interpreted yet.
Evaluation strategy:
Promise executors run at construction. Effect thunks wait for a runner.
Effect<number, Error, never> — Error is
Middle parameter is E. number is A. never here means no requirements.
Effect.sync(() => log()) logs
Repeatable: each run* re-interprets the same description.
Official React example calls runSync
useMemo holds the description. The click is the edge where the runtime runs it.
Choices shuffle. Same length on purpose.
Read this next
Primary source: The Effect Type (one page). Optional contrast: Effect vs Promise, first two sections only.
Keep the type card nearby. When you can explain the lab without looking, say so — constructors are next.