Skip to content
LogoLogo

Devtools

Use compiler diagnostics to inspect VibeLang contracts and catch failures or requirements that escape a function's promise. The vibelang compiler and its language server check .vibe and .ts files together. A VibeLang-specific Oxlint configuration is not shipped.

Install language tooling

// example.vibe
// run: Console.log(sample())
function sample() {
  sleep("5 millis")
  return "sample"
}
// string!!
// Output: sample

Install vibelang as a dev dependency. Its vibelang --lsp --stdio command starts the language server from the project's compiler, with one server for .vibe and .ts files.

For VS Code and Cursor, install the extension that ships in vibelang, then open the application directory:

code --install-extension node_modules/vibelang/editor/vibelang.vsix
# Or, for Cursor:
cursor --install-extension node_modules/vibelang/editor/vibelang.vsix

The extension is not on the VS Code Marketplace or Open VSX yet. Install it again from node_modules after you update vibelang.

Other editors configure their LSP client to run vibelang --lsp --stdio for *.vibe and *.ts, with tsconfig.json as the root marker. Turn off the other TypeScript server for this project to avoid duplicate diagnostics. JetBrains IDEs can use LSP4IJ.

For GitHub highlighting, add *.vibe linguist-language=TypeScript to .gitattributes; otherwise GitHub shows .vibe files without highlighting. The starter's .gitattributes already has this line. Effect's own @effect/tsgo tooling remains for .ts files and does not check VibeLang semantics.

Inspect inferred information

// run: Console.log(accepted(2))
error Rejected
 
function accepted(value: number) {
  if (value < 0) throw new Rejected()
  return value + 1
}
// number!Rejected!
// Output: 3

Inspect A, E, and R when changing a function. A local call returns the success value, while the function's complete contract also records escaping failures and unmet services. Generated declarations expose that complete Effect contract to TypeScript consumers.

Hover over a function to see its inferred Effect<A, E, R>; hover over a call to see its success value's type. Separate control-class hover text is a D33 design target, not a shipped feature.

Diagnostics, completions, and refactors

// expect-error: EK2001
// Rejected escapes a contract promising no failures.
function promised(value: number): number!! {
  return accepted(value)
}

The language design diagnoses this as EK2001. Handle Rejected inside the function or widen its contract. Related checks include unmet services (EK2002), running a program at module top level without an explicit Effect type on a const (EK1001), and passing a suspending callback to synchronous JavaScript (EK4301).

Inside functions, plain foreign calls need a boundary hatch such as try or sync, or a trusted declaration (EK4401). Effect's own packages and packages matching compilerOptions.vibelang.trustedPackages are trusted: plain calls are synchronous, their exceptions are defects, and Effect-returning calls run. Running a platform API is rejected with an Effect module suggestion (EK4402, D108); importing or passing its binding as a value is allowed, and uncontrolled time or randomness reads have their own diagnostic (EK4403). A runner inside code a fiber runs is EK1005, and a try block that runs Effects with E = never gets EK2004 (defects pass through), a warning by default.

EK1010 refuses an unparenthesized throw expression on the left of a binary operator (except comma), as a conditional's condition, or before as or satisfies. Write throw (…) to throw the whole expression; value ?? throw e and throw statements keep their meaning.

The server exposes TypeScript completions through the VibeLang checker, including service member types. Quick fixes repair retired spellings and the mechanical cases of Effect's language service rules, below, and refactors write a function's inferred contract, convert an async function or an interface, and convert between calls and pipes (D245). D33's catch-specific completions are not implemented. Other diagnostic messages include suggested repairs as text. Do not apply a TypeScript refactor that introduces Effect.gen or yield* inside .vibe.

Configure compiler checks

// run: Console.log(checked(2))
function checked(value: number): number!Rejected! {
  return accepted(value)
}
// Output: 3

A contract gives the compiler a promise to check. To infer selected channels, write infer Name in those slots: Effect<number, Rejected, infer R> checks the success and failure channels and infers requirements. All three slots are required in a return annotation with a body. For omitted slots, EK2003 suggests explicit never or infer contracts in its diagnostic text. Configure the compiler in tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "sourceMap": true,
    "vibelang": {
      "diagnostics": { "EK2004": "warning", "EK4403": "error" },
      "tracing": "exported",
      "trustedPackages": ["@smthrs/*"]
    }
  }
}

EK2004, EK4403, EK4406, and the rules from Effect's language service, EK4801-EK4825 except EK4813, can be set to "error", "warning", or "off"; the values shown are EK2004's and EK4403's defaults. When EK4403 is lowered, those reads compile as trusted synchronous calls that tests cannot control. Migration diagnostics such as EK2112 for retired typed catch clauses are warnings with fixed severity. Core language errors have fixed severity too. @ts-expect-error does not suppress EK diagnostics. vibelang exits non-zero on any error and reports locations in the source. tracing accepts "none" (the default), "exported", or "all" to add function spans.

trustedPackages accepts package-name globs matched against package.json names, including workspace packages. The whole name must match; *, ?, and character classes match within each slash-delimited segment. For example, "@smthrs/*" trusts packages in that scope. Omit the option to retain EK4401 for foreign calls. Use trusted(impl, [Errors]) from vibelang-runtime in TypeScript for expected failures on an individual function.

Effect language service rules

The table below accounts for the 108 diagnostics and editor features in @effect/language-service 0.87.5 (D240). Many rules address forms that VibeLang replaces: there is no Effect.gen to misuse, a call already runs, and catch catches failures. Existing EK diagnostics cover others. The remaining checks are EK4801-EK4825, with mechanical quick fixes, alongside refactors, hovers, and completions. The table also identifies features that are not implemented.

EK4801-EK4825 are lint. Each has a default, error, warning, or off, and every one except EK4813 can be set in compilerOptions.vibelang.diagnostics or by a comment (D241). A later comment overrides an earlier one, and within one comment a code named on its own overrides *:

// run: Console.log(discard(() => { Console.log("unused") ; return 42 }))
function discard(program: Effect.Effect<number>) {
  // @vibelang-diagnostics-next-line EK4801:off
  program
  return 0
}
// Output: 0

// @vibelang-diagnostics EK4818:warning applies from its line to the end of the file, and * names every adjustable code. EK4801, EK4819, and EK4820 also report in the .ts files that the program checks; for Effect's other rules in .ts files, serve them with Effect's own TypeScript 7 server, @effect/tsgo (D242).

GroupEffect's rule or featureIn VibeLangExample
CorrectnessanyUnknownInErrorContextEK4811, off by defaultthrow cause where cause: unknown
CorrectnessclassSelfMismatchNot needed: a service declaration has no Self parameterservice Users { find(id: string): User!NotFound }
CorrectnessduplicatePackageEK4803, warningtwo versions of effect in one program
CorrectnesseffectFnImplicitAnyNot needed: a VibeLang function's parameters are TypeScript's, so strict reports TS7006function load(id) { … } is TS7006
CorrectnessfloatingEffectEK4801, error, with a quick fix; also in .ts filesprogram alone on a line; the fix writes program()
CorrectnessgenericEffectServicesCovered: EK3005service Repo<T> { … } is EK3005
CorrectnessmissingEffectContextCovered: EK2002 at a contract, EK3201 at a runnerfunction f(): User!NotFound! { return Users.find(id) } is EK2002
CorrectnessmissingEffectErrorCovered: EK2001function f(): User!! { return Users.find(id) } is EK2001
CorrectnessmissingLayerContextCovered: EK2002 and EK3201; wire places providersprovide UsersLive { … } without the database layer is EK3201 at the runner
CorrectnessmissingReturnYieldStarNot needed in .vibe: throw ends the function. EK4820 in .ts filesthrow new NotFound({ id })
CorrectnessmissingStarInYieldEffectGenNot needed in .vibe: a call runs, and Effect.gen with function* is EK1002. EK4819 in .ts filesconst user = Users.find(id)
CorrectnessnonObjectEffectServiceTypeEffect 3 only, and a service declares membersservice Port { value: number }
CorrectnessoutdatedApiEK4813, error, in place of TS2339, with a quick fix; the same fix on TS2339 in .ts filesEffect.catchAll; the fix writes Effect.catch
CorrectnessoutdatedEffectCodegenNot adopted: VibeLang has no codegen commentsNone
CorrectnessoverriddenSchemaConstructorEK4814, warningclass Person extends Schema.Class<Person>("Person")({ … }) { constructor() { … } }
CorrectnessunsupportedServiceAccessorsNot needed: Users.find(id) calls a member through the key, generic or notUsers.find(id)
Anti-patterncatchUnfailableEffectCovered: EK2004, warningtry { Effect.succeed(1) } catch (e) { … } is EK2004; plain catches keep JavaScript semantics
Anti-patterneffectFnIifeNot needed: no Effect.fn; an immediately called function is a call(() => load())()
Anti-patterneffectGenUsesAdapterNot needed: no Effect.genNone
Anti-patterneffectInFailureEK4815, warningthrow program; run it: program()
Anti-patterneffectInVoidSuccessEK4802, warning, with a quick fix; TS2322 under a void!! contractreturn program from a function that runs Effects
Anti-patternglobalErrorInEffectCatchEK4804, warningtry(code, (cause) => new Error(String(cause)))
Anti-patternglobalErrorInEffectFailureEK4804, warningthrow new Error("boom"); declare error Boom { }
Anti-patternlayerMergeAllWithDependenciesEK4807, warning, with a quick fixLayer.mergeAll(DbLive, UsersLive); the fix writes wire
Anti-patternlazyPromiseInEffectSyncNot needed: sync(() => promise) already compiles to Effect.promise (D148)sync(async () => fetch(url))
Anti-patternleakingRequirementsEK4809, warning; @vibelang leakable exempts a serviceservice Users { find(): User!!Db; all(): User[]!!Db }
Anti-patternmultipleEffectProvideEK4808, warning, with a quick fixprovide A { provide B { … } }; the fix writes provide A, B { … } or provide wire(A, B) { … }
Anti-patternreturnEffectInGenEK4802, warning, with a quick fixreturn program; the fix writes return program()
Anti-patternrunEffectInsideEffectCovered: EK1005Effect.runPromise(load()) inside a function is EK1005
Anti-patternschemaSyncInEffectEffect 3 onlyNone
Anti-patternscopeInLayerEffectEffect 3 only: Effect 4's Layer.effect owns its scopeLayer.effect(Users) { using pool = open(); … }
Anti-patternstrictEffectProvideEK4816, off by defaultprovide AppLive { … } outside main
Anti-patterntryCatchInEffectGenNot needed: catch catches failurestry { Users.find(id) } catch (e) { … }
Anti-patternunknownInEffectCatchEK4806, warningtry(code, (cause) => cause)
Effect-nativeasyncFunctionCovered: EK1003async function load() { … } is EK1003
Effect-nativecryptoRandomUUIDCovered: EK4403crypto.randomUUID() is EK4403
Effect-nativecryptoRandomUUIDInEffectCovered: EK4403crypto.randomUUID() is EK4403
Effect-nativeextendsNativeErrorEK4805, warningclass Boom extends Error {}; declare error Boom { }
Effect-nativeglobalConsoleCovered: EK4402console.log(x) is EK4402
Effect-nativeglobalConsoleInEffectCovered: EK4402console.log(x) is EK4402
Effect-nativeglobalDateCovered: EK4403Date.now() is EK4403
Effect-nativeglobalDateInEffectCovered: EK4403new Date() is EK4403
Effect-nativeglobalFetchCovered: EK4402fetch(url) is EK4402
Effect-nativeglobalFetchInEffectCovered: EK4402fetch(url) is EK4402
Effect-nativeglobalRandomCovered: EK4403Math.random() is EK4403
Effect-nativeglobalRandomInEffectCovered: EK4403Math.random() is EK4403
Effect-nativeglobalTimersCovered: EK4402setTimeout(f, 10) is EK4402
Effect-nativeglobalTimersInEffectCovered: EK4402setTimeout(f, 10) is EK4402
Effect-nativeinstanceOfSchemaEK4817, off by default, with a quick fixvalue instanceof Person; the fix writes Schema.is(Person)(value)
Effect-nativenewPromiseEK4812, warningnew Promise((resolve) => …); use callback((resume) => …)
Effect-nativenodeBuiltinImportCovered: EK4402 where the code runs the import; importing is allowed (D108)readFile(path) from node:fs/promises is EK4402
Effect-nativepreferSchemaOverJsonEK4818, off by defaultJSON.parse(text)
Effect-nativeprocessEnvCovered: EK4402process.env.PORT is EK4402; use Config
Effect-nativeprocessEnvInEffectCovered: EK4402process.env.PORT is EK4402
Effect-nativeunsafeEffectTypeAssertionEK4810, warning, with a quick fixprogram as Effect.Effect<number> drops a failure
StylecatchAllToMapErrorNot needed: a catch that rethrows is the one formcatch (e) { throw new Failed({ cause: e }) }
StyledeterministicKeysNot needed: the compiler derives each service key from its package, module, and name@acme/app/src/users#Users
StyleeffectDoNotationNot needed: const statements bind valuesconst user = Users.find(id)
StyleeffectFnOpportunityNot needed: every function is onefunction load(id: string) { … }
StyleeffectMapFlattenNot needed: statementsconst a = f(); return g(a)
StyleeffectMapVoidNot needed: statementsf() on its own line
StyleeffectSucceedWithVoidNot needed: a function that returns nothingfunction done() { }
StyleflatMapToMapNot needed: statementsreturn g(f())
StyleimportFromBarrelNot needed: the compiler writes the imports for effect's root names (D146)Option.some(1) with no import
StylemissedPipeableOpportunityRefactor: "Convert to a pipe"f(g(h(x))) becomes x |> h(%) |> g(%) |> f(%)
StylemissingEffectServiceDependencyEffect 3 only; wire computes dependencieswire(UsersLive, DbLive)
StylenestedEffectGenYieldNot needed: no Effect.genNone
StyleredundantSchemaTagIdentifierNot needed: error and schema declarations take their tag from their nameerror NotFound { id: string }
StyleschemaStructWithTagEK4821, off by default, with a quick fix to Schema.TaggedStructSchema.Struct({ _tag: Schema.Literal("A"), … })
StyleschemaUnionOfLiteralsEffect 3 onlyNone
StyleserviceNotAsClassNot needed: service declarationsservice Users { … }
StylestrictBooleanExpressionsEK4822, off by defaultif (count) { … }
StyleunnecessaryArrowBlockEK4823, off by default, with a quick fix(x) => { return x + 1 }
StyleunnecessaryEffectGenNot needed: no Effect.genNone
StyleunnecessaryFailYieldableErrorNot needed: throw new NotFound({ id })throw new NotFound({ id })
StyleunnecessaryPipeEK4824, off by default, with a quick fixpipe(x)
StyleunnecessaryPipeChainEK4825, off by default, with a quick fixx.pipe(f).pipe(g)
HoverExtended Effect typeCovered: a function's hover shows its contract, A!E!R (D77)string!UserNotFound!Users
Hoveryield* hoverNot needed: a running call's hover shows its success value (D77)Runs here and returns User
HoverLayer service involvementHover: a layer lists what it provides, needs, and fails withHover on UsersLive
HoverLayer graphHover: wire lists the order it builds its layers in, and what each usesHover on wire(UsersLive, DbLive)
CompletionSelf in classesNot needed: declarations have no SelfNone
CompletionEffect.gen snippetNot needed: no Effect.genNone
CompletionEffect.fn span nameNot needed: tracing names spans after functions (D104)`"tracing": "exported",
"trustedPackages": ["@smthrs/*"]`
CompletionDuration stringsCompletion: "2 seconds" and the other units where a Duration.Input goesEffect.sleep("1 second")
CompletionNamespace import styleNot needed: effect's root names are in scope (D146)Option.some(1)
CompletionSchema.brand nameCompletion: the declared name in Schema.brand("…")schema UserId = Schema.String.pipe(Schema.brand("UserId"))
CompletionComment directives@vibelang-diagnostics comments (D241), whose codes complete// @vibelang-diagnostics-next-line EK4801:off
Refactorasync function to Effect.genRefactor: "Convert to a VibeLang function"await fetch(url) becomes try((url) => fetch(url), url)
Refactorasync function to Effect.gen, with an error per promiseRefactor: "Convert to a VibeLang function with an error per await"try(fetch(url), (cause) => new FetchFailed({ cause }))
Refactorasync function to Effect.fnRefactor: "Convert to a VibeLang function"; VibeLang has one function formAs above
Refactorasync function to Effect.fn, with an error per promiseRefactor: "Convert to a VibeLang function with an error per await"As above
RefactorEffect.gen to Effect.fnNot needed: one function formNone
RefactorImplement service accessorsNot needed: Users.find(id)None
RefactorFunction calls to pipeRefactor: "Convert to a pipe"f(g(x)) becomes x |> g(%) |> f(%)
RefactorPipe to data-firstRefactor: "Convert the pipe to calls"x |> g(%) |> f(%) becomes f(g(x))
RefactorToggle return type signatureRefactor: "Write the inferred contract" and "Remove the contract"function load(id: string): User!NotFound!Users
RefactorRemove an unnecessary Effect.genNot needed: no Effect.genNone
RefactorWrap with Effect.genNot needed: every function is oneNone
RefactorToggle pipe stylesRefactor: "Write pipe(x, …)" and "Write x.pipe(…)"x.pipe(f, g) and pipe(x, f, g)
RefactorLayer MagicNot needed: wire composes layers in dependency order (D27)wire(AppLive, UsersLive, DbLive)
RefactorStructural type to SchemaRefactor: "Convert to a schema declaration"interface User { name: string } becomes schema User { name: string }
RefactorEffect.Service to Context.TagNot needed: service declarationsNone
CodegenannotateRefactor: "Write the inferred contract"function load(id: string): User!NotFound!Users
CodegenaccessorsNot needed: Users.find(id)None
CodegentypeToSchemaRefactor: "Convert to a schema declaration"schema User { name: string }
OtherRenaming a tagged class renames its tagNot needed: an error declaration's tag is its name, so a rename renames botherror NotFound { } renamed to Missing
OtherGo to definition for RpcClientNot adopted yet: the language server has no definition hookNone

Lint and build integration

Run vibelang --noEmit for type checking. The compiler owns type and language diagnostics in the terminal, editor, and Vite overlay. The local vibelang-vite package supports Vite 7 and 8 and Vitest 5. It runs one whole-project compiler subprocess on the first request and after edits, serves JavaScript and source maps, and invalidates loaded .vibe modules and their importers. It does not use vibelang --api.

// TypeScript (.ts)
// vitest.config.ts
import { defineConfig } from "vitest/config"
import vibelang from "vibelang-vite"
 
export default defineConfig({
  plugins: [vibelang()],
  test: { include: ["src/**/*.test.vibe"] }
})

The starter already includes this configuration. bun run test checks and compiles source tests without a prior build. See TestClock for tests with virtual time.

D35 assigns syntactic linting to Oxlint and reserves type-aware rules for .ts files. vibelang-oxlint-config and a tested Vite Plus integration are not shipped. Keep type-aware linting off .vibe: a stock TypeScript checker models different call semantics.

Debugging in editors

// run: Console.log(tracedSample())
function tracedSample() {
  return withSpan(sample(), "sample")
}
// Output: sample

Add spans where you need an execution boundary to appear in telemetry. VibeLang functions are untraced by default; tracing can add spans to exported functions or named functions and methods that compile to frames. Anonymous functions receive no automatic span. Effect's DevTools layer from effect/devtools, with its VS Code extension, shows fibers and logical span stacks across suspensions.

vibelang with sourceMap enabled and the Vite plugin produce maps from emitted JavaScript directly to .vibe positions. Debuggers that read standard source maps map breakpoints and paused frames to .vibe: VS Code's JavaScript debugger, Chrome and Edge DevTools, and JetBrains IDEs.

Compiled functions and producers retain their source names; clauses use the operation member's name, and region closures use names such as sample$try, $catch, $finally, and $using. The JavaScript stack shows the active frame above Effect's run loop. A paused continuation is a heap value, so its frames are absent while a clause runs and become active again when it resumes. Stepping over a pausing call can enter the run loop; set a breakpoint on the next source line. Use spans for the logical call chain, and include the Effect version, inferred contract, and diagnostic when reporting a problem.

Coming from Effect

Library debugging concepts still apply. VibeLang's call execution, service declarations, and source mapping need language-aware tooling beyond the upstream TypeScript plugin.


The matching Effect page: effect.website/docs/v4/getting-started/devtools