VibeLang
VibeLang gives Effect a simple, readable syntax that compiles to TypeScript, with first-class access to the TypeScript ecosystem.
npm create vibelang@latest my-app
cd my-app
npm run startbun create vibelang my-app
cd my-app
bun run startOverview
VibeLang is Effect as its own language, written in TypeScript syntax. Effect puts what a function returns, what can fail, and what it depends on into one type, Effect<Success, Error, Requirements>, and the compiler checks it. As a library, Effect builds every feature out of generators, yield*, pipe, and classes. VibeLang gives each Effect idea its own syntax, and its compiler builds the same Effect program you would have written by hand.
Every function is an Effect
interface User { id: string; name: string }
error UserNotFound { id: string }
service Users {
find(id: string): User!UserNotFound
}
function greet(id: string) {
const name = Users.find(id).name catch "guest"
return `Hello, ${name}`
}
// string!!Usersclass UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", {
id: Schema.String
}) {}
class Users extends Context.Service<Users, {
find(id: string): Effect.Effect<User, UserNotFound>
}>()("app/Users") {}
const greet = Effect.fn("greet")(function* (id: string) {
const users = yield* Users
const user = yield* users.find(id).pipe(
Effect.catchTag("UserNotFound", () => Effect.succeed({ name: "guest" }))
)
return `Hello, ${user.name}`
})The comment under greet is the type your editor shows. string!!Users is VibeLang's spelling of Effect<string, never, Users>: greet returns a string, cannot fail, and needs the Users service. The compiler infers all three from the body, and TypeScript code that imports greet sees the Effect type.
Calls run in order, throw fails with a typed error, catch handles it, service declares a dependency, and fork starts a fiber.
Retries, timeouts, and concurrency
error ApiError { status: number }
service Users {
name(id: string): string!ApiError
}
function nameOf(id: string) {
return Users.name(id)
|> retry(%, Schedule.exponential("100 millis"))
|> timeout(%, "2 seconds")
catch "unknown"
}
export function names(ids: ReadonlyArray<string>) {
const all = forEach(ids, nameOf, { concurrency: 8 })
log(`Loaded ${all.length} names`)
return all
}import { Context, Effect, Schedule, Schema } from "effect"
class ApiError extends Schema.TaggedError<ApiError>()("ApiError", {
status: Schema.Number
}) {}
class Users extends Context.Service<Users, {
name(id: string): Effect.Effect<string, ApiError>
}>()("app/Users") {}
const nameOf = Effect.fn("nameOf")(function* (id: string) {
const users = yield* Users
return yield* users.name(id).pipe(
Effect.retry(Schedule.exponential("100 millis")),
Effect.timeout("2 seconds"),
Effect.orElseSucceed(() => "unknown")
)
})
export const names = Effect.fn("names")(function* (ids: ReadonlyArray<string>) {
const all = yield* Effect.forEach(ids, nameOf, { concurrency: 8 })
yield* Effect.log(`Loaded ${all.length} names`)
return all
})Both versions compile. The pipe operator |> passes Users.name(id) to retry and timeout by name, so they can run it again. catch "unknown" removes every failure, including the timeout, so nameOf is string!!Users. forEach with { concurrency: 8 } loads up to eight names at once.
Hello, VibeLang
Create a project from the starter. You need Node 26 or later and Bun 1.4 or later.
npm create vibelang@latest my-app
cd my-app
npm run startThe starter's two source files:
export error EmptyName
export function greeting(name: string) {
if (name.length === 0) throw new EmptyName()
return `Hello, ${name}!`
}import { EmptyName, greeting } from "./greeting.vibe"
service Greeter {
greet(name: string): string!EmptyName
}
const GreeterLive = Layer.succeed(Greeter, {
greet(name: string) { return greeting(name) }
})
export function main() {
provide GreeterLive {
Console.log(Greeter.greet("VibeLang"))
try {
Greeter.greet("")
} catch (error) {
Console.log(`Recovered from ${error._tag}`)
}
}
}npm run start compiles the project and runs main, which prints:
Hello, VibeLang!
Recovered from EmptyNameGetting Started walks through the starter, and Installation covers editors and existing projects.
TypeScript stays one step away
VibeLang's compiler is TypeScript's compiler, extended. It checks .vibe and .ts files together and emits JavaScript and .d.ts files, so npm packages, your .ts files, and every Effect library work unchanged. TypeScript code calls VibeLang functions as ordinary Effect functions.
When you need plain TypeScript, sync, try, and callback take a plain TypeScript function, and a .ts file can sit beside any .vibe file.
function slug(title: string) {
return sync((title) => {
// Plain TypeScript: nothing in here is an Effect.
let s = title.trim().toLowerCase()
s = s.replaceAll(/[^a-z0-9]+/g, "-")
return s
}, title)
}
// string!!Features
Everything Effect does, VibeLang does, because the output is Effect. The common features have their own syntax:
| Effect feature | VibeLang |
|---|---|
| Effect types | User!NotFound!Users: returns User, fails with NotFound, needs Users |
| Typed errors | error NotFound { id: string }, throw new NotFound({ id }), x catch fallback |
| Dependency injection | service Users { ... }, provide UsersLive { ... } |
| Structured concurrency | fork, join, await.all [a(), b()], for concurrent(8) (const id of ids) |
| Retries and timeouts | work() |> retry(%, policy) |> timeout(%, "1 second") |
| Resource safety | using conn = connect(), finally |
| Tracing | withSpan("checkout") { ... } |
| Schema validation | schema User { id: string; role: "admin" | "member" } |
| Pattern matching | match (e) { when NotFound: ...; } |
Streams, configuration, and Effect's HTTP, SQL, CLI, and AI packages need no syntax of their own: call them as you would in TypeScript. The cheat sheet puts every VibeLang form beside the Effect code it replaces.
Built for coding agents
When coding agents write a growing share of your code, the hard part moves from writing code to trusting it.
- The compiler checks what agents write. Every function's type lists what it can fail with and what it needs, and code that breaks a declared type does not compile.
- People can review what agents write. VibeLang code is about half the size of the Effect code it replaces, and it reads as plain TypeScript.
- Agents write less. Fewer tokens per program means less to generate and less context to read back.
Where to go next
- Coming from Effect: the cheat sheet shows every change, and VibeLang for Effect users explains the rules.
- New to Effect: start with Welcome, then The Effect Type and Expected Errors.
- Set up a project or editor: Set up VibeLang covers new and existing projects, Vite, React, Vue, Svelte, VS Code, Neovim, and Zed.
VibeLang is in early alpha. The language will change in major, breaking ways, and it is not ready for production use. Version 0.0.1 is tested on macOS on Apple silicon and on Linux on x64.