Warning: Effeck is v0. The language will change in major, breaking ways. It is not suitable for production use.

Devtools

Use compiler diagnostics to inspect Effeck contracts and catch failures or requirements that escape a function's promise. The effeck compiler and its language server check .effeck and .ts files together; Oxlint supplies lint findings.

Install language tooling

// example.effeck
function sample() {
  sleep("5 millis")
  return "sample"
}
// string!!

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

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

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

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

Other editors configure their LSP client to run effeck --lsp --stdio for *.effeck 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 *.effeck linguist-language=TypeScript to .gitattributes; otherwise GitHub shows .effeck 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 Effeck semantics.

Inspect inferred information

error Rejected

function accepted(value: number) {
  if (value < 0) throw new Rejected()
  return value + 1
}
// number!Rejected!

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 (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 are trusted. 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 catch that can never receive an expected failure is EK2004, a warning by default.

The server exposes TypeScript completions through the Effeck 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 .effeck.

Configure compiler checks

function checked(value: number): number!Rejected! {
  return accepted(value)
}

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,
    "effeck": {
      "diagnostics": { "EK2004": "warning", "EK4403": "error" },
      "tracing": "exported"
    }
  }
}

EK2004, EK4403, 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. All other EK diagnostics are errors whose severity cannot change, and @ts-expect-error does not suppress them. effeck exits non-zero on any error and reports locations in the source. tracing accepts "none" (the default), "exported", or "all" to add function spans.

Effect language service rules

Effeck supports every feature of Effect's language service, @effect/language-service 0.87.5 (D240). Most of its 108 rules guard against mistakes that Effeck's syntax rules out: there is no Effect.gen to misuse, a call already runs, and catch catches failures. Existing EK diagnostics cover others. The rest are EK4801-EK4825, with quick fixes where the repair is mechanical, and refactors, hovers, and completions in the editor.

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

function discard(program: Effect.Effect<number>) {
  // @effeck-diagnostics-next-line EK4801:off
  program
  return 0
}

// @effeck-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 EffeckExample
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: an Effeck 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 .effeck: throw ends the function. EK4820 in .ts filesthrow new NotFound({ id })
CorrectnessmissingStarInYieldEffectGenNot needed in .effeck: 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: Effeck has no codegen commentsNone
CorrectnessoverriddenSchemaConstructorEK4814, errorclass 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 { pure() } catch (e) { … } is EK2004
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; @effeck 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"
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@effeck-diagnostics comments (D241), whose codes complete// @effeck-diagnostics-next-line EK4801:off
Refactorasync function to Effect.genRefactor: "Convert to an Effeck function"await fetch(url) becomes try((url) => fetch(url), url)
Refactorasync function to Effect.gen, with an error per promiseRefactor: "Convert to an Effeck function with an error per await"try(fetch(url), (cause) => new FetchFailed({ cause }))
Refactorasync function to Effect.fnRefactor: "Convert to an Effeck function"; Effeck has one function formAs above
Refactorasync function to Effect.fn, with an error per promiseRefactor: "Convert to an Effeck 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 effeck --noEmit for type checking. The compiler owns type and language diagnostics in the terminal, editor, and Vite overlay. The local effeck-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 .effeck modules and their importers. It does not use effeck --api.

// TypeScript (.ts)
// vitest.config.ts
import { defineConfig } from "vitest/config"
import effeck from "effeck-vite"

export default defineConfig({
  plugins: [effeck()],
  test: { include: ["src/**/*.test.effeck"] }
})

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. effeck-oxlint-config and a tested Vite Plus integration are not shipped. Keep type-aware linting off .effeck: a stock TypeScript checker models different call semantics.

Debugging in editors

function tracedSample() {
  return withSpan(sample(), "sample")
}

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

effeck with sourceMap enabled and the Vite plugin produce maps from emitted JavaScript directly to .effeck positions. Debuggers that read standard source maps map breakpoints and paused frames to .effeck: 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. Effeck's call execution, service declarations, and source mapping need language-aware tooling beyond the upstream TypeScript plugin.

Upstream Effect page