Cheat sheet
Every Effeck change next to the Effect code it replaces. The left column is Effect 4 TypeScript, and the right column is Effeck. The specification states the rules behind each change. The examples share these declarations:
error NotFound { id: string }
interface User { id: string; name: string }
function getUser(id: string): User!NotFound {
sleep("5 millis")
if (id === "missing") throw new NotFound({ id })
return { id, name: "Ada" }
}
function getOrders(id: string): number!! {
return id.length
}
control Approval {
approve(request: string): boolean!!
}
function release(version: string) {
return Approval.approve(`deploy ${version}`) ? `released ${version}` : "declined"
}Types
Effect types (D127). An empty slot is never. Dependencies left out are inferred from a function's body, and are never anywhere else.
type Find = (id: string) => Effect.Effect<User, NotFound>
type Release = (version: string) => Effect.Effect<string, never, Approval>
type Load = () => Effect.Effect<string>
const hello = (id: string): Effect.Effect<string, NotFound> =>
Effect.map(getUser(id), (user) => `Hello, ${user.name}`)type Find = (id: string) => User!NotFound
type Release = (version: string) => string!!Approval
type Load = () => string!!
function hello(id: string): string!NotFound {
return `Hello, ${getUser(id).name}`
}Declarations
Errors (D126).
class Missing extends Data.TaggedError("Missing")<{ readonly id: string }> {}
class Timeout extends Data.TaggedError("Timeout") {}error Missing { id: string }
error TimeoutServices (D128, D223). Members have no bodies, and value members are readonly.
class Users extends Context.Service<Users, {
find(id: string): Effect.Effect<User, NotFound>
readonly region: string
}>()("app/Users") {}service Users {
find(id: string): User!NotFound
region: string
}Control operations (D128, D223). Effect has none, so the left column is Effeck's long form.
abstract class Review extends Control {
abstract approve(request: string): Effect<boolean, never, never>
}control Review {
approve(request: string): boolean!!
}Layers (D221). A layer is Effect's own call: the block after Layer.effect(Users) is its effect (block params, D188), its statements run once when the layer builds, and the service types the returned members. Without setup, write Layer.succeed(Users, { ... }).
const UsersLive = Layer.effect(Users, Effect.gen(function* () {
const seen = new Set<string>()
return Users.of({ find: (id) => getUser(id), region: "eu" })
}))const UsersLive = Layer.effect(Users) {
const seen = new Set<string>()
return {
find(id) { return getUser(id) },
region: "eu"
}
}Schemas (D133). One declaration gives the schema and its type.
const Account = Schema.Struct({
id: Schema.String,
nickname: Schema.optional(Schema.String),
role: Schema.Literals(["admin", "member"])
})
interface Account extends Schema.Schema.Type<typeof Account> {}schema Account {
id: string
nickname?: string
role: "admin" | "member"
}Readonly by default (D134). Write mutable for a member you assign.
interface Cart { readonly id: string; items: Array<string> }interface Cart { id: string; mutable items: Array<string> }Programs
Statements instead of generators. Calls run; there is no yield*.
const greetUser = Effect.fn("greetUser")(function* (id: string) {
const user = yield* getUser(id)
yield* Effect.log(`Hello, ${user.name}`)
return user.name
})function greetUser(id: string) {
const user = getUser(id)
log(`Hello, ${user.name}`)
return user.name
}Programs passed by name (D147). A call runs where it stands, and is passed as a program where a parameter expects one.
const user = yield* getUser("1")
const first = yield* Effect.race(getUser("1"), getUser("2"))function fastest() {
const user = getUser("1")
return race(getUser("1"), getUser("2"))
}Providing layers (D130). return inside the block returns from the function.
const program = Effect.gen(function* () {
const users = yield* Users
return yield* users.find("1")
}).pipe(Effect.provide(UsersLive))function program() {
provide UsersLive {
return Users.find("1")
}
}Providing one service (D173, D174). provide Key = value provides one implementation without a layer; layers and services mix in one list, and an effect module such as Random stands for its key, Random.Random.
const seeded = { nextIntUnsafe: () => 4, nextDoubleUnsafe: () => 0.5 }
const roll = Random.nextInt.pipe(
Effect.provideService(Random.Random, seeded),
Effect.provide(Logger.layer([]))
)const seeded = { nextIntUnsafe: () => 4, nextDoubleUnsafe: () => 0.5 }
function roll(): number!! {
provide Logger.layer([]), Random = seeded {
return Random.nextInt()
}
}Catching by tag (D216). Catch the failure and match on it. A match that names every failure needs no rethrow; default: throw e passes on the ones it does not name.
const describe = (id: string) =>
Effect.map(getUser(id), (user) => user.name).pipe(
Effect.catchTag("NotFound", (e) => Effect.succeed(`no user ${e.id}`))
)function describe(id: string) {
try {
return getUser(id).name
} catch (e) {
return match (e) {
when NotFound: `no user ${e.id}`;
}
}
}Falling back in an expression (D236). x catch fallback runs the fallback only after an expected failure; defects and interrupts pass through.
const nameOrGuest = (id: string) =>
Effect.map(getUser(id), (user) => user.name).pipe(
Effect.orElseSucceed(() => "guest")
)function nameOrGuest(id: string) {
return getUser(id).name catch "guest"
}Catching in an expression (D236). x catch (e) => … passes the failure to a handler, which can match on it; a match that names every failure removes them all.
const nameOrReason = (id: string) =>
Effect.map(getUser(id), (user) => user.name).pipe(
Effect.catch((e) => Effect.succeed(`no user ${e.id}`))
)function nameOrReason(id: string) {
return getUser(id).name catch (e) => match (e) {
when NotFound: `no user ${e.id}`;
}
}Handling control operations (D132). Effect has none, so the left column is Effeck's long form.
function auto(version: string) {
return handle(() => release(version), Approval, {
approve(_request, continuation) {
return resume(continuation, true)
}
})
}function auto(version: string) {
try {
return release(version)
} handle Approval.approve(request) {
resume(true)
}
}Entry point (D135). An exported main runs when Node starts its module, and not when a test imports it.
// main.ts
export const main = Effect.log("Hello")
// run.ts
NodeRuntime.runMain(main)export function main() {
log("Hello")
}Causes, exits, and interruption
Catch every cause (D217). match on exit(…): a Failure exit's cause also holds defects and interrupts.
const safe = (id: string) =>
Effect.map(getUser(id), (user) => user.name).pipe(
Effect.catchCause((cause) => Effect.succeed(`failed: ${Cause.pretty(cause)}`))
)function safe(id: string) {
return match (exit(getUser(id))) {
when Exit.Success(let user): user.name;
when Exit.Failure(let cause): `failed: ${Cause.pretty(cause)}`;
}
}Rethrow a cause (D177). throw with a Cause fails with every reason it holds.
const logged = getUser("1").pipe(
Effect.catchCause((cause) => Effect.andThen(Effect.logError(cause), Effect.failCause(cause)))
)function logged() {
const outcome = exit(getUser("1"))
if (outcome is Exit.Failure(let cause)) {
logError(cause)
throw cause
}
return outcome.value
}Run on every exit (D178). The block runs without interruption; a success carries no value.
const tracked = getUser("1").pipe(
Effect.onExit((exit) => Effect.log(`lookup ${exit._tag}`))
)function tracked() {
try {
return getUser("1")
} finally (exit) {
log(`lookup ${exit._tag}`)
}
}Exit, yield, and interrupt (D179). exit takes its program by name.
const result = yield* Effect.exit(getUser("1"))
yield* Effect.yieldNow
if (result._tag === "Failure") yield* Effect.interruptfunction outcome() {
const result = exit(getUser("1"))
yieldNow()
if (result is Exit.Failure) interrupt()
return result
}Interruption (D180). The blocks keep return and break meaning what they mean in the function; uninterruptible(program) also works.
const guarded = Effect.uninterruptibleMask((restore) =>
Effect.gen(function* () {
const user = yield* getUser("1")
const orders = yield* restore(getOrders(user.id))
return { user, orders }
})
)
const cancellable = Effect.interruptible(getOrders("1"))function guarded() {
uninterruptible {
const user = getUser("1")
restore {
return { user, orders: getOrders(user.id) }
}
}
}
function cancellable() {
interruptible {
return getOrders("1")
}
}Calls
No imports from effect (D146).
import { Console } from "effect"
const ready = Console.log("ready")function ready() {
Console.log("ready")
}Common functions without the Effect. prefix (D151): sleep, log, retry, repeat, timeout, race, all, forEach, die, withSpan, and the log levels.
const patient = Effect.gen(function* () {
yield* Effect.sleep("10 millis")
return yield* Effect.timeout(Effect.retry(getUser("1"), Schedule.recurs(2)), "1 second")
})function patient() {
sleep("10 millis")
return timeout(retry(getUser("1"), Schedule.recurs(2)), "1 second")
}Methods on Effect's data types (D152).
const hot = Stream.take(
Stream.map(Stream.filter(Stream.make(18, 25, 30), (c) => c >= 25), (c) => `${c} C`),
2
)function hot() {
return Stream.make(18, 25, 30).filter((c) => c >= 25).map((c) => `${c} C`).take(2)
}The pipe operator (D154). % is the value on the left; where it lands in a program parameter, the left side is passed as a program.
const piped = getUser("1").pipe(
Effect.retry(Schedule.recurs(2)),
Effect.timeout("1 second")
)function piped() {
return getUser("1") |> retry(%, Schedule.recurs(2)) |> timeout(%, "1 second")
}TC39 forms
Throw in an expression (D186, throw expressions).
const findUser = (users: Map<string, User>, id: string) =>
Effect.fromNullishOr(users.get(id)).pipe(Effect.mapError(() => new NotFound({ id })))function findUser(users: Map<string, User>, id: string) {
return users.get(id) ?? throw new NotFound({ id })
}Blocks as values (D187, do expressions). The value is the last statement's.
const label = (id: string) =>
Effect.map(getUser(id), (user) => (user.name.length > 3 ? user.name : "short"))function label(id: string) {
const name = do {
const user = getUser(id)
if (user.name.length > 3) user.name
else "short"
}
return name
}Blocks after calls (D188, block params). The block is the call's program; it needs return for a value.
const loaded = Effect.withSpan(
Effect.gen(function* () {
const user = yield* getUser("1")
return user.name
}),
"load"
)
it.effect("finds a user", () =>
Effect.gen(function* () {
const user = yield* getUser("1")
assert.strictEqual(user.name, "Ada")
}))import { assert, it } from "@effect/vitest"
function loaded() {
return withSpan("load") {
const user = getUser("1")
return user.name
}
}
it.effect("finds a user") {
const user = getUser("1")
assert.strictEqual(user.name, "Ada")
}Pattern matching
From the TC39 pattern matching proposal (stage 1). See Pattern matching with match and is.
Match a value (D206, D209). Without default, a missing case of a finite union is EK4701, as with Match.exhaustive.
const phaseLabel = (phase: "draft" | "published") =>
Match.value(phase).pipe(
Match.when("draft", () => "Draft"),
Match.when("published", () => "Published"),
Match.exhaustive
)function phaseLabel(phase: "draft" | "published") {
return match (phase) {
when "draft": "Draft";
when "published": "Published";
}
}Match errors by class (D215). An error class is a pattern; and { let id } binds a property.
const describeFailure = (failure: NotFound | Timeout) =>
Match.value(failure).pipe(
Match.tag("NotFound", (e) => `no user ${e.id}`),
Match.tag("Timeout", () => "retry"),
Match.exhaustive
)function describeFailure(failure: NotFound | Timeout) {
return match (failure) {
when NotFound and { let id }: `no user ${id}`;
when Timeout: "retry";
}
}Option, Exit, and Result (D208). The arms can run Effect functions.
const greet = (name: Option.Option<string>) =>
Option.match(name, { onNone: () => "Hello", onSome: (n) => `Hello, ${n}` })
const failed = (outcome: Exit.Exit<User, NotFound>) => Exit.isFailure(outcome)function greet(name: Option.Option<string>) {
return match (name) {
when Option.Some(let n): `Hello, ${n}`;
when Option.None: "Hello";
}
}
function failed(outcome: Exit.Exit<User, NotFound>) {
return outcome is Exit.Failure
}Test and bind (D211). Bindings in an is work in an if or while condition.
const firstFailure = (outcome: Exit.Exit<User, NotFound>) =>
Exit.isFailure(outcome) && Cause.hasFails(outcome.cause) ? Cause.pretty(outcome.cause) : "ok"function firstFailure(outcome: Exit.Exit<User, NotFound>) {
if (outcome is Exit.Failure(let cause) && Cause.hasFails(cause)) return Cause.pretty(cause)
return "ok"
}Escape hatches
Plain, self-contained TypeScript inside sync, try, and callback (D148, D229-D232). A throw in sync is a defect; in try it is an expected failure. Async bodies are Effect.promise and Effect.tryPromise. A hatch's function reads values of the .effeck file only as inputs, the arguments after it, which fill its first parameters; try's error mapper stays second (EK4405). runPromise leads back in from a hatch's body or a .ts file.
const id = Effect.sync(() => crypto.randomUUID())
const parsed = Effect.try({ try: () => JSON.parse(text), catch: (error) => new BadJson({ error }) })
const loaded = Effect.promise(() => Promise.resolve(42))
const fetched = Effect.tryPromise((signal) => fetch(url, { signal }))
const tick = Effect.callback<number>((resume) => {
const timer = setTimeout(() => resume(Effect.succeed(7)), 10)
return Effect.sync(() => clearTimeout(timer))
})
const name = await Effect.runPromise(greetUser("1"))error BadJson { error: unknown }
function hatches(text: string, url: string) {
const id = sync(crypto.randomUUID())
const parsed = try((text) => JSON.parse(text), (error) => new BadJson({ error }), text)
const loaded = sync(async () => Promise.resolve(42))
const fetched = try(async (url, signal) => fetch(url, { signal }), url)
const tick = callback<number>((resume) => {
const timer = setTimeout(() => resume(7), 10)
return () => clearTimeout(timer)
})
const name = sync(async (greet) => runPromise(greet("1")), greetUser)
return { id, parsed, loaded, status: fetched.status, tick, name }
}Fibers
Fork and join (D166, D225). A fiber belongs to the block that forks it. join [a, b] joins several and interrupts the rest on a failure.
const user = yield* Effect.forkChild(getUser("1"))
const orders = yield* Effect.forkChild(getOrders("1"))
return [yield* Fiber.join(user), yield* Fiber.join(orders)]function both() {
const user = fork getUser("1")
const orders = fork getOrders("1")
return [join user, join orders]
}Wait for several (D196). await.all behaves like Promise.all: it starts each element in its own fiber, keeps the shape, and a failure interrupts the rest.
const [user, orders] = yield* Effect.all([getUser("1"), getOrders("1")], { concurrency: "unbounded" })
const pair = yield* Effect.all({ user: getUser("1"), orders: getOrders("1") }, { concurrency: "unbounded" })function many() {
const [user, orders] = await.all [getUser("1"), getOrders("1")]
const pair = await.all { user: getUser("1"), orders: getOrders("1") }
return { user, orders, pair }
}First success, every outcome (D196). await.any behaves like Promise.any and await.allSettled like Promise.allSettled.
const fastest = yield* Effect.raceAll([getUser("1"), getUser("2")])
const outcomes = yield* Effect.all([getUser("1"), getOrders("1")], { concurrency: "unbounded", mode: "result" })function outcomes() {
const fastest = await.any [getUser("1"), getUser("2")]
const results = await.allSettled [getUser("1"), getOrders("1")]
return { fastest, results }
}Fork a block (D166). The loop stops when the enclosing block exits.
const withHeartbeat = Effect.scoped(Effect.gen(function* () {
yield* Effect.forkScoped(Effect.forever(Effect.andThen(Effect.log("tick"), Effect.sleep("1 second"))))
return yield* getUser("1")
}))function withHeartbeat() {
fork {
while (true) {
log("tick")
sleep("1 second")
}
}
return getUser("1")
}Detached and scoped fibers (D170).
yield* Effect.forkDetach(getUser("1"))
const scope = yield* Scope.make()
const fiber = yield* Effect.forkIn(getUser("2"), scope)function background() {
detach(getUser("1"))
const scope = Scope.make()
const fiber = forkIn(scope, getUser("2"))
Scope.close(scope, Exit.void)
return fiber
}Wait for a Deferred (D227). await waits for promises in JavaScript, so a Deferred uses its method.
const ready = yield* Deferred.make<string>()
yield* Deferred.succeed(ready, "go")
const value = yield* Deferred.await(ready)function signal() {
const ready = Deferred.make<string>()
Deferred.succeed(ready, "go")
return ready.await()
}Race (D196, D197). await.race behaves like Promise.race: the first to finish wins, success or failure, and the rest are interrupted. It replaced select (EK4607); poll with Queue.poll and an if.
const next = yield* Effect.raceFirst(
Effect.map(Queue.take(jobs), (job) => `ran ${job}`),
Effect.as(Effect.sleep("1 second"), "timeout")
)function next() {
const jobs = Queue.unbounded<string>()
Queue.offer(jobs, "build")
return await.race [
`ran ${Queue.take(jobs)}`,
do {
sleep("1 second")
"timeout"
}
]
}A concurrent loop (D169). At most n items run at once, and all finish before the loop ends.
yield* Effect.forEach(ids, (id) => Effect.flatMap(getUser(id), (user) => Effect.log(user.name)), {
concurrency: 8,
discard: true
})function logAll(ids: string[]) {
for concurrent(8) (const id of ids) log(getUser(id).name)
}