Skip to content
LogoLogo

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:

VibeLang
// 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, Ada

Declarations

Errors (D126).

error Missing { id: string }
error Timeout

Services (D128, D223). Members have no bodies, and value members are readonly.

service Users {
  find(id: string): User!NotFound
  region: string
}

Control operations (D128, D223). Effect has none, so the left column is VibeLang's long form.

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, { ... }).

// 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: 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"}

Readonly by default (D134). Write mutable for a member you assign.

interface Cart { id: string; mutable 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: Ada

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: Ada

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"}

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: 4

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"]

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"]

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"]

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

Entry 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

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: true

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"}

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: Ada

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: Success

Interruption (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]

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: 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"}

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"]

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"}

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: Failure

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: 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: 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"]

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"]

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: false

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: true

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"}

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]

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}}

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: true

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: Ada

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: true

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: go

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 build

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: Ada