Before writing any code, understand what Effect actually is. It's NOT just another utility library — it's a complete paradigm for describing programs.
Effect<A, E, R>Lazy executionReferential transparencyWhat gets printed to the console? Remember: Effects are lazy blueprints, not eager Promises.
import { Effect } from "effect"
const a = Effect.sync(() => console.log("hello"))
const b = Effect.succeed(42)
console.log("done")
// What prints? (A) "hello" then "done" (B) "done" only (C) nothing// Answer: (B) "done" only
// Effect.sync wraps a function but does NOT execute it.
// Effect.succeed wraps a value — no side effect at all.
// Only console.log("done") runs because it's plain TS, not an Effect.Fill in the A (success), E (error), and R (requirements) for each Effect type.
import { Effect } from "effect"
// What are A, E, R for each?
const a = Effect.succeed("hello")
// Effect<???, ???, ???>
const b = Effect.fail(new Error("boom"))
// Effect<???, ???, ???>
const c = Effect.sync(() => Math.random())
// Effect<???, ???, ???>import { Effect } from "effect"
const a = Effect.succeed("hello")
// Effect<string, never, never>
// A=string (the value), E=never (can't fail), R=never (no deps)
const b = Effect.fail(new Error("boom"))
// Effect<never, Error, never>
// A=never (never succeeds), E=Error, R=never
const c = Effect.sync(() => Math.random())
// Effect<number, never, never>
// A=number (return type of the fn), E=never (sync assumes no throw), R=neverLearn the constructors — how to wrap existing values, sync code, and async code into the Effect world.
Effect.succeed(value)Effect.fail(error)Effect.sync(() => ...)Effect.try(() => ...)Effect.promise(() => ...)Effect.tryPromise(() => ...)// Wrapping existing values
const success = Effect.succeed(42) // Effect<number, never, never>
const failure = Effect.fail("oh no") // Effect<never, string, never>
// Wrapping sync code
const random = Effect.sync(() => Math.random())
const parsed = Effect.try(() => JSON.parse(input))
// Wrapping async code
const fetched = Effect.tryPromise({
try: () => fetch("https://api.example.com/data"),
catch: (err) => new HttpError({ cause: err })
})For each scenario, choose the correct Effect constructor. Think about: does it throw? Is it async? Do you already have the value?
import { Effect } from "effect"
// 1. You have a config object already loaded
const config = { port: 3000 }
const getConfig = Effect.???(config)
// 2. Reading from localStorage (sync, might throw in SSR)
const getToken = Effect.???(() => localStorage.getItem("token"))
// 3. Calling an external API (async, might reject)
const fetchUser = Effect.???(() => fetch("/api/user"))
// 4. Getting the current date (sync, never throws)
const now = Effect.???(() => new Date())import { Effect } from "effect"
// 1. Already have a value → succeed
const config = { port: 3000 }
const getConfig = Effect.succeed(config)
// 2. Sync code that might throw → try
const getToken = Effect.try(() => localStorage.getItem("token"))
// 3. Async code that might reject → tryPromise
const fetchUser = Effect.tryPromise(() => fetch("/api/user"))
// 4. Sync code that won't throw → sync
const now = Effect.sync(() => new Date())Convert this vanilla TS function into an Effect. JSON.parse can throw, so pick the right constructor and map the error.
import { Effect } from "effect"
// Vanilla TS — throws on invalid JSON
function parseJson(raw: string) {
return JSON.parse(raw)
}
// TODO: rewrite as an Effect that captures the error
const parseJsonEffect = (raw: string) =>
Effect.???(/* your code here */)import { Effect } from "effect"
// Why is (raw: string) outside Effect.try?
// Effect.try(...) returns a single Effect — a blueprint for one computation.
// The outer function parameterizes it: raw is captured via closure.
// This is the same pattern as: const fn = (x) => new Promise(r => r(x))
// You'll see this everywhere in Effect: regular function outside, Effect inside.
const parseJsonEffect = (raw: string) =>
Effect.try({
try: () => JSON.parse(raw),
catch: (error) => new Error(`Invalid JSON: ${error}`)
})
// Type: (raw: string) => Effect<unknown, Error, never>Effects are blueprints. Runners execute them. Choose the right runner for your context.
Effect.runSync(effect)Effect.runPromise(effect)Effect.runPromiseExit(effect)Effect.runFork(effect)// One runner at the edge of your program
const program = Effect.gen(function* () {
yield* Effect.log("Starting...")
return yield* myApp
})
// Pick the right runner:
Effect.runSync(program) // sync only, throws on async/failure
Effect.runPromise(program) // returns Promise, rejects on failure
Effect.runPromiseExit(program) // returns Promise<Exit>, never rejectsEach runner behaves differently on success, failure, and async effects. Predict what happens in each case.
import { Effect } from "effect"
const success = Effect.succeed(42)
const failure = Effect.fail("oops")
const async_ = Effect.tryPromise(() => fetch("https://example.com"))
// What does each return or throw?
Effect.runSync(success) // → ???
Effect.runSync(failure) // → ???
Effect.runSync(async_) // → ???
Effect.runPromise(success) // → ???
Effect.runPromise(failure) // → ???import { Effect } from "effect"
Effect.runSync(success) // → 42
Effect.runSync(failure) // → throws (runSync throws when the Effect fails)
Effect.runSync(async_) // → throws (runSync can't handle async Effects)
Effect.runPromise(success) // → Promise that resolves to 42
Effect.runPromise(failure) // → Promise that rejects with "oops"This code crashes. Figure out why, and fix it by choosing a different runner.
import { Effect } from "effect"
const fetchData = Effect.tryPromise(
() => fetch("https://api.example.com/data")
)
// This crashes — why?
const result = Effect.runSync(fetchData)
console.log(result)import { Effect } from "effect"
const fetchData = Effect.tryPromise(
() => fetch("https://api.example.com/data")
)
// runSync can't handle async Effects — fetch returns a Promise.
// Fix: use runPromise, which returns a Promise itself.
Effect.runPromise(fetchData).then(console.log)Effect.gen is how you write sequential Effect code that reads like async/await. This is the syntax you'll use 90% of the time.
Effect.gen(function* () { ... })yield*Return valueconst program = Effect.gen(function* () {
const user = yield* fetchUser(id) // like: await fetchUser(id)
const posts = yield* fetchPosts(user.id)
return { user, posts }
})
// program is Effect<{ user, posts }, Error, never>
// Nothing has executed yet — it's still just a blueprint!Rewrite this async function using Effect.gen. Replace await with yield*, and wrap the fetch call properly.
// Original async/await code
async function getUser(id: string) {
const res = await fetch(`/api/users/${id}`)
const user = await res.json()
return user.name
}
// TODO: rewrite using Effect.gen
import { Effect } from "effect"
const getUser = (id: string) =>
Effect.gen(function* () {
// your code here
})import { Effect } from "effect"
const getUser = (id: string) =>
Effect.gen(function* () {
const res = yield* Effect.tryPromise({
try: () => fetch(`/api/users/${id}`),
catch: () => new Error("fetch failed")
})
const user = yield* Effect.tryPromise({
try: () => res.json(),
catch: () => new Error("invalid JSON")
})
return user.name as string
})
// Type: (id: string) => Effect<string, Error, never>
// The catch mapper controls what goes in the E channel.
// Nothing runs until you call a runner!Use Effect.gen to sequence three independent effects and combine their results into one object.
import { Effect } from "effect"
const getName = Effect.succeed("Alice")
const getAge = Effect.succeed(30)
const getRole = Effect.succeed("admin")
// TODO: use Effect.gen to combine into { name, age, role }
const getProfile = Effect.gen(function* () {
// your code here
})
// Then run it:
// Effect.runSync(getProfile)
// → { name: "Alice", age: 30, role: "admin" }import { Effect } from "effect"
const getName = Effect.succeed("Alice")
const getAge = Effect.succeed(30)
const getRole = Effect.succeed("admin")
const getProfile = Effect.gen(function* () {
const name = yield* getName // no parentheses — getName is already an Effect
const age = yield* getAge // same as: await myPromise (not myPromise())
const role = yield* getRole // use () only when calling a function that RETURNS an Effect
return { name, age, role }
})
console.log(Effect.runSync(getProfile))
// → { name: "Alice", age: 30, role: "admin" }