Cheat sheet
Every VibeLang change next to the Effect code it replaces. The left column is Effect 4 TypeScript, and the right column is VibeLang. The specification states the rules behind each change. The examples share these declarations:
// run: Console.log(JSON.stringify([getUser("1"), getOrders("1"), handle(() => release("v1"), Approval, { approve: (_request, k) => resume(k, true) })]))
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"
}
// Output: [{"id":"1","name":"Ada"},1,"released v1"]Types
Effect types (D127). An empty slot is never. Dependencies left out are inferred from a function's body, and are never anywhere else.
// run: Console.log(hello("1"))
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}`
}
// Output: Hello, Adatype 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}`)Declarations
Errors (D126).
error Missing { id: string }
error Timeoutclass Missing extends Data.TaggedError("Missing")<{ readonly id: string }> {}
class Timeout extends Data.TaggedError("Timeout") {}Services (D128, D223). Members have no bodies, and value members are readonly.
service Users {
find(id: string): User!NotFound
region: string
}class Users extends Context.Service<Users, {
find(id: string): Effect.Effect<User, NotFound>
readonly region: string
}>()("app/Users") {}Control operations (D128, D223). Effect has none, so the left column is VibeLang's long form.
control Review {
approve(request: string): boolean!!
}abstract class Review extends Control {
abstract approve(request: string): Effect<boolean, never, never>
}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, { ... }).
// run: provide UsersLive { Console.log(JSON.stringify(Users.find("1"))); Console.log(Users().region) }
const UsersLive = Layer.effect(Users) {
const seen = new Set<string>()
return {
find(id) { return getUser(id) },
region: "eu"
}
}
// Output: {"id":"1","name":"Ada"}
// Output: euconst UsersLive = Layer.effect(Users, Effect.gen(function* () {
const seen = new Set<string>()
return Users.of({ find: (id) => getUser(id), region: "eu" })
}))Schemas (D133). One declaration gives the schema and its type.
// run: Console.log(JSON.stringify(Schema.decodeUnknownEffect(Account)({ id: "1", role: "admin" })))
schema Account {
id: string
nickname?: string
role: "admin" | "member"
}
// Output: {"id":"1","role":"admin"}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> {}Readonly by default (D134). Write mutable for a member you assign.
interface Cart { id: string; mutable items: Array<string> }interface Cart { readonly id: string; items: Array<string> }Programs
Statements instead of generators. Calls run; there is no yield*.
// run: Console.log(Effect.provide(() => greetUser("1"), Logger.layer([sync(() => Logger.make(({ message }) => console.log(...(message as unknown[]))))])))
function greetUser(id: string) {
const user = getUser(id)
Console.log(`Hello, ${user.name}`)
return user.name
}
// Output: Hello, Ada
// Output: Adaconst greetUser = Effect.fn("greetUser")(function* (id: string) {
const user = yield* getUser(id)
yield* Effect.Console.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.
// run: Console.log(fastest().name)
function fastest() {
const user = getUser("1")
return race(getUser("1"), getUser("2"))
}
// Output: Adaconst user = yield* getUser("1")
const first = yield* Effect.race(getUser("1"), getUser("2"))Providing layers (D130). return inside the block returns from the function.
// run: Console.log(JSON.stringify(program()))
function program() {
provide UsersLive {
return Users.find("1")
}
}
// Output: {"id":"1","name":"Ada"}const program = Effect.gen(function* () {
const users = yield* Users
return yield* users.find("1")
}).pipe(Effect.provide(UsersLive))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.
// run: Console.log(roll())
const seeded = { nextIntUnsafe: () => 4, nextDoubleUnsafe: () => 0.5 }
function roll(): number!! {
provide Logger.layer([]), Random = seeded {
return Random.nextInt()
}
}
// Output: 4const seeded = { nextIntUnsafe: () => 4, nextDoubleUnsafe: () => 0.5 }
const roll = Random.nextInt.pipe(
Effect.provideService(Random.Random, seeded),
Effect.provide(Logger.layer([]))
)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.
// run: Console.log(JSON.stringify([describe("1"), describe("missing")]))
function describe(id: string) {
try {
return getUser(id).name
} catch (e) {
return match (e) {
when NotFound: `no user ${e.id}`;
}
}
}
// Output: ["Ada","no user missing"]const describe = (id: string) =>
Effect.map(getUser(id), (user) => user.name).pipe(
Effect.catchTag("NotFound", (e) => Effect.succeed(`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.
// run: Console.log(JSON.stringify([nameOrGuest("1"), nameOrGuest("missing")]))
function nameOrGuest(id: string) {
return getUser(id).name catch "guest"
}
// Output: ["Ada","guest"]const nameOrGuest = (id: string) =>
Effect.map(getUser(id), (user) => user.name).pipe(
Effect.orElseSucceed(() => "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.
// run: Console.log(JSON.stringify([nameOrReason("1"), nameOrReason("missing")]))
function nameOrReason(id: string) {
return getUser(id).name catch (e) => match (e) {
when NotFound: `no user ${e.id}`;
}
}
// Output: ["Ada","no user missing"]const nameOrReason = (id: string) =>
Effect.map(getUser(id), (user) => user.name).pipe(
Effect.catch((e) => Effect.succeed(`no user ${e.id}`))
)Handling control operations (D132). Effect has none, so the left column is VibeLang's long form.
// run: Console.log(auto("v1"))
function auto(version: string) {
try {
return release(version)
} handle Approval.approve(request) {
resume(true)
}
}
// Output: released v1// run: Console.log(auto("v1"))
function auto(version: string) {
return handle(() => release(version), Approval, {
approve(_request, continuation) {
return resume(continuation, true)
}
})
}
// Output: released v1Entry point (D135). An exported main runs when Node starts its module, and not when a test imports it.
// run: Effect.provide(main(), Logger.layer([sync(() => Logger.make(({ message }) => console.log(...(message as unknown[]))))]))
export function main() {
log("Hello")
}
// Output: Hello// main.ts
export const main = Effect.log("Hello")
// run.ts
NodeRuntime.runMain(main)Causes, exits, and interruption
Catch every cause (D217). match on exit(…): a Failure exit's cause also holds defects and interrupts.
// run: Console.log(safe("1")); Console.log(safe("missing").includes("NotFound"))
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)}`;
}
}
// Output: Ada
// Output: trueconst safe = (id: string) =>
Effect.map(getUser(id), (user) => user.name).pipe(
Effect.catchCause((cause) => Effect.succeed(`failed: ${Cause.pretty(cause)}`))
)Rethrow a cause (D177). throw with a Cause fails with every reason it holds.
// run: Console.log(JSON.stringify(logged()))
function logged() {
const outcome = exit(getUser("1"))
if (outcome is Exit.Failure(let cause)) {
logError(cause)
throw cause
}
return outcome.value
}
// Output: {"id":"1","name":"Ada"}const logged = getUser("1").pipe(
Effect.catchCause((cause) => Effect.andThen(Effect.logError(cause), Effect.failCause(cause)))
)Run on every exit (D178). The block runs without interruption; a success carries no value.
// run: Console.log(Effect.provide(() => tracked().name, Logger.layer([sync(() => Logger.make(({ message }) => console.log(...(message as unknown[]))))])))
function tracked() {
try {
return getUser("1")
} finally (exit) {
log(`lookup ${exit._tag}`)
}
}
// Output: lookup Success
// Output: Adaconst tracked = getUser("1").pipe(
Effect.onExit((exit) => Effect.log(`lookup ${exit._tag}`))
)Exit, yield, and interrupt (D179). exit takes its program by name.
// run: Console.log(outcome()._tag)
function outcome() {
const result = exit(getUser("1"))
yieldNow()
if (result is Exit.Failure) interrupt()
return result
}
// Output: Successconst result = yield* Effect.exit(getUser("1"))
yield* Effect.yieldNow
if (result._tag === "Failure") yield* Effect.interruptInterruption (D180). The blocks keep return and break meaning what they mean in the function; uninterruptible(program) also works.
// run: Console.log(JSON.stringify([guarded(), cancellable()]))
function guarded() {
uninterruptible {
const user = getUser("1")
restore {
return { user, orders: getOrders(user.id) }
}
}
}
function cancellable() {
interruptible {
return getOrders("1")
}
}
// Output: [{"user":{"id":"1","name":"Ada"},"orders":1},1]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"))Calls
A tagged template is a call of its tag (D267). If sql returns an Effect, the statement sql`CREATE TABLE example (id TEXT)` runs it, and const rows = sql`SELECT * FROM example` receives its success value. Plain tags keep JavaScript behavior. A discarded structured Effect subtype, such as SQL Statement, reports EK4301: call the result explicitly, or annotate the tag as returning Effect<A, E, R>. A parameter that expects a program takes an Effectful template by name, just as it takes a call (D147).
No imports from effect (D146).
// run: ready()
function ready() {
Console.log("ready")
}
// Output: readyimport { Console } from "effect"
const 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.
// run: Console.log(JSON.stringify(patient()))
function patient() {
sleep("10 millis")
return timeout(retry(getUser("1"), Schedule.recurs(2)), "1 second")
}
// Output: {"id":"1","name":"Ada"}const patient = Effect.gen(function* () {
yield* Effect.sleep("10 millis")
return yield* Effect.timeout(Effect.retry(getUser("1"), Schedule.recurs(2)), "1 second")
})Methods on Effect's data types (D152).
// run: Console.log(JSON.stringify(Stream.runCollect(hot())))
function hot() {
return Stream.make(18, 25, 30).filter((c) => c >= 25).map((c) => `${c} C`).take(2)
}
// Output: ["25 C","30 C"]const hot = Stream.take(
Stream.map(Stream.filter(Stream.make(18, 25, 30), (c) => c >= 25), (c) => `${c} C`),
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.
// run: Console.log(JSON.stringify(piped()))
function piped() {
return getUser("1") |> retry(%, Schedule.recurs(2)) |> timeout(%, "1 second")
}
// Output: {"id":"1","name":"Ada"}const piped = getUser("1").pipe(
Effect.retry(Schedule.recurs(2)),
Effect.timeout("1 second")
)TC39 forms
Throw in an expression (D186, throw expressions).
// run: Console.log(findUser(new Map([["1", { id: "1", name: "Ada" }]]), "1").name); Console.log(Effect.result(findUser(new Map<string, User>(), "missing"))._tag)
function findUser(users: Map<string, User>, id: string) {
return users.get(id) ?? throw new NotFound({ id })
}
// Output: Ada
// Output: Failureconst findUser = (users: Map<string, User>, id: string) =>
Effect.fromNullishOr(users.get(id)).pipe(Effect.mapError(() => new NotFound({ id })))Blocks as values (D187, do expressions). The value is the last statement's.
// run: Console.log(label("1"))
function label(id: string) {
const name = do {
const user = getUser(id)
if (user.name.length > 3) user.name
else "short"
}
return name
}
// Output: shortconst label = (id: string) =>
Effect.map(getUser(id), (user) => (user.name.length > 3 ? user.name : "short"))Blocks after calls (D188, block params). The block is the call's program; it needs return for a value.
// run: Console.log(loaded())
function loaded() {
return withSpan("load") {
const user = getUser("1")
return user.name
}
}
// Output: Adaconst loaded = Effect.withSpan(
Effect.gen(function* () {
const user = yield* getUser("1")
return user.name
}),
"load"
)
it.live("finds a user", () =>
Effect.gen(function* () {
const user = yield* getUser("1")
assert.strictEqual(user.name, "Ada")
}))// block-params.test.vibe
import { assert, it } from "@effect/vitest"
it.live("finds a user") {
const user = getUser("1")
assert.strictEqual(user.name, "Ada")
}
// Output:it.live uses the live clock because this page's getUser sleeps. it.effect supplies a test clock; a test that sleeps there must advance it with TestClock.adjust.
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.
// run: Console.log(JSON.stringify([phaseLabel("draft"), phaseLabel("published")]))
function phaseLabel(phase: "draft" | "published") {
return match (phase) {
when "draft": "Draft";
when "published": "Published";
}
}
// Output: ["Draft","Published"]const phaseLabel = (phase: "draft" | "published") =>
Match.value(phase).pipe(
Match.when("draft", () => "Draft"),
Match.when("published", () => "Published"),
Match.exhaustive
)Match errors by class (D215). An error class is a pattern; and { let id } binds a property.
// run: Console.log(JSON.stringify([describeFailure(new NotFound({ id: "7" })), describeFailure(new Timeout())]))
function describeFailure(failure: NotFound | Timeout) {
return match (failure) {
when NotFound and { let id }: `no user ${id}`;
when Timeout: "retry";
}
}
// Output: ["no user 7","retry"]const describeFailure = (failure: NotFound | Timeout) =>
Match.value(failure).pipe(
Match.tag("NotFound", (e) => `no user ${e.id}`),
Match.tag("Timeout", () => "retry"),
Match.exhaustive
)Option, Exit, and Result (D208). The arms can run Effect functions.
// run: Console.log(greet(Option.some("Ada"))); Console.log(greet(Option.none())); Console.log(failed(Exit.fail(new NotFound({ id: "7" })))); Console.log(failed(Exit.succeed({ id: "1", name: "Ada" })))
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
}
// Output: Hello, Ada
// Output: Hello
// Output: true
// Output: falseconst greet = (name: Option.Option<string>) =>
Option.match(name, { onNone: () => "Hello", onSome: (n) => `Hello, ${n}` })
const failed = (outcome: Exit.Exit<User, NotFound>) => Exit.isFailure(outcome)Test and bind (D211). Bindings in an is work in an if or while condition.
// run: Console.log(firstFailure(Exit.succeed({ id: "1", name: "Ada" }))); Console.log(firstFailure(Exit.fail(new NotFound({ id: "7" }))).includes("NotFound"))
function firstFailure(outcome: Exit.Exit<User, NotFound>) {
if (outcome is Exit.Failure(let cause) && Cause.hasFails(cause)) return Cause.pretty(cause)
return "ok"
}
// Output: ok
// Output: trueconst firstFailure = (outcome: Exit.Exit<User, NotFound>) =>
Exit.isFailure(outcome) && Cause.hasFails(outcome.cause) ? Cause.pretty(outcome.cause) : "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 .vibe 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.
// run: provide Logger.layer([sync(() => Logger.make(({ message }) => console.log(...(message as unknown[]))))]) { const value = hatches('{"ok":true}', "data:text/plain,hello"); Console.log(JSON.stringify({ ...value, id: /^[0-9a-f-]{36}$/.test(value.id) })) }
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 }
}
// Output: Hello, Ada
// Output: {"id":true,"parsed":{"ok":true},"loaded":42,"status":200,"tick":7,"name":"Ada"}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"))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.
// run: Console.log(JSON.stringify(both()))
function both() {
const user = fork getUser("1")
const orders = fork getOrders("1")
return [join user, join orders]
}
// Output: [{"id":"1","name":"Ada"},1]const user = yield* Effect.forkChild(getUser("1"))
const orders = yield* Effect.forkChild(getOrders("1"))
return [yield* Fiber.join(user), yield* Fiber.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.
// run: Console.log(JSON.stringify(many()))
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 }
}
// Output: {"user":{"id":"1","name":"Ada"},"orders":1,"pair":{"user":{"id":"1","name":"Ada"},"orders":1}}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" })First success, expected outcomes (D196, D258). await.any waits for the first success. await.allSettled returns successes and typed failures as Result values, rather than JavaScript's settlement objects, and never fails with a typed error. Defects and interruption propagate exactly as in Effect.all with mode: "result"; a cause with no typed failure still propagates. Wrap each element in exit(...) when every cause must become data.
// run: const value = outcomes(); Console.log(value.fastest.name); Console.log(JSON.stringify(value.results.map((result) => result._tag))); Console.log(JSON.stringify((await.allSettled [getUser("missing"), getOrders("1")]).map((result) => result._tag))); Console.log(Exit.isFailure(exit(await.allSettled [die("defect"), getOrders("1")])))
function outcomes() {
const fastest = await.any [getUser("1"), getUser("2")]
const results = await.allSettled [getUser("1"), getOrders("1")]
return { fastest, results }
}
// Output: Ada
// Output: ["Success","Success"]
// Output: ["Failure","Success"]
// Output: trueconst fastest = yield* Effect.raceAll([getUser("1"), getUser("2")])
const outcomes = yield* Effect.all([getUser("1"), getOrders("1")], { concurrency: "unbounded", mode: "result" })Fork a block (D166). The loop stops when the enclosing block exits.
// run: provide Logger.layer([]) { Console.log(withHeartbeat().name) }
function withHeartbeat() {
fork {
while (true) {
log("tick")
sleep("1 second")
}
}
return getUser("1")
}
// Output: Adaconst withHeartbeat = Effect.scoped(Effect.gen(function* () {
yield* Effect.forkScoped(Effect.forever(Effect.andThen(Effect.log("tick"), Effect.sleep("1 second"))))
return yield* getUser("1")
}))Detached and scoped fibers (D170).
// run: Console.log(Exit.isFailure(Fiber.await(background())))
function background() {
detach(getUser("1"))
const scope = Scope.make()
const fiber = forkIn(scope, getUser("2"))
Scope.close(scope, Exit.void)
return fiber
}
// Output: trueyield* Effect.forkDetach(getUser("1"))
const scope = yield* Scope.make()
const fiber = yield* Effect.forkIn(getUser("2"), scope)Wait for a Deferred (D227). await waits for promises in JavaScript, so a Deferred uses its method.
// run: Console.log(signal())
function signal() {
const ready = Deferred.make<string>()
Deferred.succeed(ready, "go")
return ready.await()
}
// Output: goconst ready = yield* Deferred.make<string>()
yield* Deferred.succeed(ready, "go")
const value = yield* Deferred.await(ready)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.
// run: Console.log(next())
function next() {
const jobs = Queue.unbounded<string>()
Queue.offer(jobs, "build")
return await.race [
`ran ${Queue.take(jobs)}`,
do {
sleep("1 second")
"timeout"
}
]
}
// Output: ran buildconst next = yield* Effect.raceFirst(
Effect.map(Queue.take(jobs), (job) => `ran ${job}`),
Effect.as(Effect.sleep("1 second"), "timeout")
)A concurrent loop (D169). At most n items run at once, and all finish before the loop ends.
// run: Effect.provide(logAll(["1", "2"]), Logger.layer([sync(() => Logger.make(({ message }) => console.log(...(message as unknown[]))))]))
function logAll(ids: string[]) {
for concurrent(8) (const id of ids) log(getUser(id).name)
}
// Output: Ada
// Output: Adayield* Effect.forEach(ids, (id) => Effect.flatMap(getUser(id), (user) => Effect.log(user.name)), {
concurrency: 8,
discard: true
})