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

28. Tooling

bun create effeck my-app        # or: npm create effeck@latest my-app
cd my-app
bun run build
bun run start
bun run test
bun run dev
bun x --no-install effeck --noEmit

Effeck 0.0.1 installs from npm as unscoped packages: effeck, effeck-runtime, effeck-vite, create-effeck, and one effeck-<os>-<cpu> package per platform. create-effeck copies the starter, pins the Effeck packages to its own version, and installs dependencies; --no-install skips installation. Projects need Node 26 or later and Bun 1.4 or later. The compiler is a native binary: effeck lists one package per platform as an optional dependency, and the package manager installs the one that matches the machine, the per-platform distribution D30 planned:

PlatformPackageTested for 0.0.1
macOS on Apple siliconeffeck-darwin-arm64Yes
macOS on Inteleffeck-darwin-x64No
Linux on x64effeck-linux-x64Yes
Linux on arm64effeck-linux-arm64No
Windows on x64effeck-win32-x64No
Windows on arm64effeck-win32-arm64No

To work on Effeck itself, install from a checkout instead. sh scripts/setup.sh requires Go, Bun, and Node on PATH; the tested versions are Go 1.26 (with Go 1.27 downloaded automatically), Bun 1.4.1, and Node 26.10. Setup builds packages/compiler/native/effeck, installs and builds the runtime, and copies templates/app into ~/.bun-create/effeck, a local template that bun create effeck prefers over the npm package. That template uses absolute file: dependencies on the checkout's compiler and Vite packages and on the runtime tarball that setup packs (D120). Keep the checkout available; after updating it, rerun setup and run bun update effeck-runtime effeck effeck-vite in projects created from it.

Setup links ~/.local/bin/effeck when that directory is already on PATH and no unrelated executable occupies the path. Project scripts find their own compiler without that global link. EFFECK_BINARY overrides the launcher's native binary. Setup preserves unrelated templates and never edits shell configuration.

Packages and versions

PackageCurrent implementation
effeckThe compiler and language server: a Node launcher that forwards arguments, streams, signals, and exit status to the native binary of the matching platform package (EFFECK_BINARY overrides it), plus the VS Code extension at editor/effeck.vsix
effeck-<os>-<cpu>The native effeck binary for one platform
effeck-runtimeContinuation kernel, prelude, and generated-code helpers; see its README for the ABI
effeck-viteWhole-project compiler subprocess integration for Vite 7 and 8 and Vitest 5
create-effeckScaffolds the starter for bun create effeck and npm create effeck
VS Code extensionShips inside effeck as editor/effeck.vsix; not on the VS Code Marketplace or Open VSX

The starter pins effect, @effect/platform-node, and @effect/vitest to 4.0.5, and Vitest to 5.0.5. The runtime checks Effect's version on load. The compiler is the TypeScript 7.1 checker, with ordinary TypeScript semantics for .ts files (D30). Its emitted declarations are ordinary .d.ts files.

effeck accepts TypeScript compiler flags: bare effeck checks and emits, --noEmit checks, --watch watches, and -b builds references. The same native binary exposes --lsp --stdio and the upstream --api transport. The current Vite plugin uses subprocess compilation instead of the API transport (D105, amending D31).

Building and testing

The starter's configuration emits Node ESM into dist/:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "sourceMap": true,
    "rootDir": "src",
    "outDir": "dist",
    "noEmitOnError": true,
    "rewriteRelativeImportExtensions": true,
    "skipLibCheck": true,
    "types": []
  },
  "include": ["src/**/*.effeck"]
}

build runs effeck. start builds and runs Node with --enable-source-maps. dev builds once, then runs the compiler watcher and Node output watcher; Ctrl+C stops both. Libraries can add declaration and declarationMap to emit declarations. The starter does not enable them.

Import local sources as ./welcome.effeck or ./format.ts. The compiler emits .js specifiers and declarations that name .js outputs (D31). In .ts, ./welcome.js resolves to welcome.effeck. Only .effeck sources are selected by the starter's include glob; imported .ts integrations also join the program.

The starter tests source files through the Vite plugin:

TypeScript
// 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"] }
})

Register it.effect from @effect/vitest at the top level or in describe (D38). Effect's packages are trusted, so call assert from @effect/vitest directly: assert.strictEqual(actual, expected), whose failure is a defect that fails the test. Keep another library's assertions inside a hatch, with the values as inputs: sync((actual) => check(actual), actual). bun run test needs no prior build and maps assertion failures to .effeck lines.

For a Vite application, add the same plugin to vite.config.ts. The plugin runs one compiler subprocess for the project on the first request and after source changes. Requests share compilation. It emits JavaScript and source maps into its own directory under node_modules/.effeck/, reports errors in the terminal and overlay, and invalidates all loaded .effeck modules and their importers after an edit. It does not compare exported contracts or use the API transport yet. Sources must stay under Vite's root so emit paths and source maps agree (D107). Its README documents project and compiler overrides.

The editor

The server is the fork's own, effeck --lsp --stdio, from the project's compiler (D32). The same binary identifies as Effeck 0.0.1 (TypeScript 7.1.0-dev) with --version; there is no separate compiler entry point (D75). effeck ships the extension as editor/effeck.vsix. Install it from a project:

code --install-extension node_modules/effeck/editor/effeck.vsix
# Or use cursor --install-extension with the same path.

Open the project directory so the extension finds its local compiler. Install the extension again after updating effeck. In a checkout, setup also builds the extension and prints its path.

The extension finds effeck from effeck.server.path, then the nearest node_modules/.bin/effeck, then PATH. It requires a trusted workspace (D79). Its grammar reuses VS Code's TypeScript grammar under source.effeck, with inner .ts scopes so existing themes apply.

For .ts ownership, disable VS Code's built-in TypeScript extension, or use Cursor's useTsgo setting (D76). The extension serves .effeck while the built-in TypeScript extension remains active, but leaves .ts to that extension. Do not set typescript.validate.enable to false: this server reads that setting too and would hide its Effeck diagnostics.

Other editors can configure their LSP client for .effeck and .ts, with tsconfig.json as the root marker, and disable their other TypeScript server for this project. Add *.effeck linguist-language=TypeScript to .gitattributes for GitHub highlighting.

Hover shows a running call's success value as "Runs here and returns T", and a plain helper's direct signature alongside its TypeScript-facing type (D77). Prelude names and service members complete in scope (D78). A method of an Effect data type hovers as the function it calls, "Method syntax: calls Option.getOrElse(maybe, …)", and completes after a dot (D152). A call passed by name hovers as "Passed by name to fork as a program" (D147), and so does a pipe's topic that passes its left side by name; any other topic hovers as its value, "(topic) %: T" (D155). TypeScript files retain TypeScript hover behavior. D33's control-class hover text and catch-specific completions remain design targets. Quick fixes exist for retired spellings and for the EK48xx rules whose repair is mechanical (D240). Refactors, code actions of kind refactor.rewrite, write or remove a function's contract, convert an interface to a schema declaration, convert an async function to an Effeck function, with or without an error declaration per await, convert nested calls to a |> pipe and back, and toggle x.pipe(f) and pipe(x, f) (D245). A layer's hover lists what it provides, needs, and fails with, and wire's hover the order it builds its layers in; a Duration.Input string, the name in Schema.brand("…"), and the codes of an @effeck-diagnostics comment complete (D246). Diagnostic messages include repair suggestions as text; the current compiler does not implement those editor refactors.

Diagnostics

Every EK diagnostic before EK4801 is an error, except seven. Each error marks code that has no meaning, or code whose generated output would break a rule of Parts II and III, so none can be lowered and // @ts-expect-error does not apply to them; it keeps its TypeScript meaning for TypeScript's own codes. Five warnings mark retired spellings that still compile the same: EK1009, the old with spelling of a provide block; EK2112, a catch clause that tests a failure's type; EK2113, a catch clause typed Cause; EK2120, a service class or control class declaration; and EK2121, a layer declaration. Each has a quick fix. The other two exceptions are policy, not soundness: EK2004, a catch statement or catch expression that can never run, is a warning by default, and EK4403, a clock or randomness read, is an error by default. A project may set either to "error", "warning", or "off" in compilerOptions.effeck.diagnostics; an EK4403 read then compiles as a trusted sync call that tests cannot control.

EK4801-EK4825 carry the rules of Effect's language service that Effeck's syntax does not already rule out (D240). They are lint, not soundness: each has a default category, error, warning, or off, and every one except EK4813, which stands in for TypeScript's TS2339, can be set in compilerOptions.effeck.diagnostics or by a comment (D241):

// @effeck-diagnostics EK4818:warning            from here to the end of the file
// @effeck-diagnostics-next-line EK4801:off       the next line only
// @effeck-diagnostics *:off                      every adjustable code

A later comment overrides an earlier one, and within one comment a code named on its own overrides *. EK4801, EK4819, and EK4820 also apply to .ts files that the program checks outside node_modules (D242).

Option in compilerOptions.effeckValuesDefault
diagnostics"EK2004", "EK4403", and EK4801-EK4825 except "EK4813", each "error", "warning", or "off"EK2004 "warning", EK4403 "error"; EK48xx as the table below says
tracing"none": no spans. "exported": exported functions compile with Effect.fn spans. "all": every named lowered function and method gets a span; anonymous lambdas do not (D104)"none"

effeck exits non-zero when any error is reported. The CLI prints error EK1003:; LSP sends source EK and numeric code 1003, which VS Code displays as EK(1003) (D61). Diagnostics print at .effeck spans, in the same form as the examples in this document, in the terminal, the editor, and Vite's overlay.

The diagnostic registry reserves these codes. This table describes each rule and the intended repair (D33, D53). A registry entry does not prove that its check or editor code action is implemented; the acceptance suite and compiler README track coverage. Repairs below are suggestions, not shipped code actions:

CodeWhat it reportsSuggested repairChapter
EK1001A module's top level runs a programExport a function instead13
EK1002A function* passed to an API whose parameter returns a library Generator or Iterator, such as Effect.gen (D64)Convert the function10
EK1003async, for await, or await using, and await inside an async function outside a hatch: a call already waitsRemove them; await in Effeck code waits for fibers (EK4601-EK4604)10
EK1004using at a module's top level or in a class static blockMove it into a function6
EK1005A runner inside code that a fiber runsCall the program, or fork it13
EK1006A pipe whose body does not use its topic, as in name |> "anonymous" (D156)Write % where the value goes: name |> greet(%)5
EK1007% outside every pipe's body (D156)Use % only after |>; % between two operands is still the remainder5
EK1008A pipe that passes its left side by name to one program and also uses it as a value (D155)Bind the value first with const, or pass it by name everywhere5, 13
EK1009A provide block written with its old keyword, with (a warning; D172)Write provide; the quick fix replaces the keyword4
EK2001An expected failure escapes the written contractCatch it, widen the contract, or Effect.orDie2, 14
EK2002A requirement escapes the written contractWiden the contract, or provide the service14
EK2003An Effect return annotation with missing channel slots, or a plain annotation on a non-method function whose body runs an Effect (D67, D74)For Effect<T>, suggest T!! or removing the annotation; for a bare T, suggest T!! or T!Errors14
EK2004A catch that can never run, a statement or a catch expression (D238) (a warning by default)Use the form that can fail, or try3
EK2005A resumeWith program that needs a service or fails outside the operation's contractRead the service in the clause first9
EK2006A return, break, or continue that leaves a finally block, which in JavaScript discards the failure in flightMove it after the try statement17
EK2101A declaration without a body that has no full contractWrite T!Errors!Dependencies, or T!!14
EK2102A recursive Effect function whose success, catch-variable, or return type needs a contract before its cycle's channels are known (D71, D72)Write the contract14
EK2103A member of a service or control declaration with a body or an initializer (D128)Implement it in a layer, or make it static4
EK2104A setup statement of an old layer declaration that follows one of its members (D129)Move the statement above the members4
EK2105A retired typed catch clause whose type has a member without a literal _tag (D131)Catch a tagged error, or test the value in an untyped clause3
EK2106A retired catch clause after one that catches every failure: an untyped clause or a Cause clause (D131, D176)Move that clause last3
EK2107A handle clause whose control is not a control declaration, such as a service (D132)Provide a service with a layer; handle only a control's operations9
EK2108A handle statement that leaves an operation of a control it handles without a clause, or gives one two clauses (D132)Add the missing clause, or remove the second9
EK2109A field of a schema declaration whose type has no schema form, or names a value that is not a schema (D133)Declare the schema with Schema.Struct, or name a schema constant11
EK2111A restore block that no uninterruptible block of the same function encloses (D180)Put it inside uninterruptible { … }, or use interruptible { … }6
EK2112Warning: a catch clause that tests a failure's type, catch (e: NotFound), retired (D216)catch (e) and test e with match or is; the quick fix rewrites the clauses3
EK2113Warning: a catch clause typed Cause, retired (D217)match (exit(…)) with Exit.Success and Exit.Failure arms; the quick fix rewrites try { return X } catch (c: Cause) { return Y }3
EK2114A catch expression, x catch y or x catch (e) => …, inside a hatch body, which is plain TypeScript (D239)Move the catch out of the hatch, or use a try statement inside it3
EK2120A warning: a service class or control class declaration, the older spelling (D223)Write service or control without class; the quick fix removes class4
EK2121A warning: a layer declaration, the old spelling of a layer (D221)Write Layer.effect(Service) { … } or Layer.succeed(Service, { … }); the quick fix rewrites it4
EK3001Two services share a keyRename one4
EK3002A class extends a serviceReplace extends with implements4
EK3003new on a serviceUse Users() for the implementation4
EK3004instanceof on a serviceNone: a service is a key, not a base class4
EK3005A generic service classUse an interface for the generic type4
EK3006A static member with the same name as a service memberRename one4
EK3101A provider looks a service up per call where the contract allows no requirementsCapture the service when the layer is built, or declare it4
EK3201No provider for a required service, with the path that needs itAdd the provider the program defines to wire4
EK3202Two providers for one serviceRemove one4
EK3203Providers form a cycleNone4
EK3204A wire argument whose type does not name its outputsCompose it with Layer.provide4
EK4201A control operation inside a combinator that runs it on another fiberHandle the operation inside the branch9, 16
EK4202A TypeScript function whose R holds a control operationNone in v09
EK4301A callback that can pause or fail where a plain function is expectedRewrite as Effect.forEach or for...of10, 16
EK4401A plain TypeScript call in code a fiber runs, with no boundary or trusted contract (D100)Wrap it in try, or tag the declaration @effeck trusted10
EK4402Running a Node.js or browser API; importing and passing values is allowed (D108)Use the Effect module the error names11
EK4403A clock or randomness read (an error by default)Use Clock, DateTime, or Random10
EK4404try given Effect's { try, catch } options object, which it would treat as a valuePass the code and the error mapping as two arguments: try((text) => parse(text), (error) => …, text)10
EK4405A hatch's TypeScript reads a binding the .effeck file declares outside the hatch: a local, a parameter, or a module declaration (D229)Pass the value as an input, an argument after the function that fills its first parameter: sync((text) => text.length, text); the quick fix does this10
EK4501A method of an Effect data type named without a call, as in const f = stream.filter (D153)Call it, or pass Stream.filter itself13
EK4601await before a call that already runs (D226)Remove await; to run the call concurrently, fork it and join the fiber7
EK4602await on a promise in Effeck code (D226)Wrap it in a hatch: try(async () => …)7
EK4603await on any other value; Effeck code has only the await operations (D226)Fork the program and join the fiber, or use await.all, await.race, await.any, or await.allSettled7
EK4604await in a hatch body that is not async (D226)Make the hatch's function async7
EK4605break that would leave a for concurrent loop (D169)Use continue to end one item, or throw to stop every item7
EK4606return in a for concurrent body (D169)Use continue to end one item7
EK4607select, which await.race replaced (D197)Write await.race [a(), b()]; for a default: clause, poll with Queue.poll and an if7
EK4608await.race or await.any on an object (D196)Write the elements in an array: await.race [a(), b()]7
EK4609await on a fiber, or an array or object of fibers (D226)Write join for await (quick fix)7
EK4610await on a Deferred (D226)Write d.await() (quick fix)7
EK4611join on a value that is not a Fiber or an array or object of fibers (D225)Join fibers; wait for a Deferred with d.await(); fork a program first7
EK4612join in a hatch body, which is plain TypeScript (D225)Move the join out of the hatch7
EK4701A match without a default arm over a finite union (tagged errors, Option, Exit, Result, literals) that misses a case (D209)Add an arm for each case left, or a default arm3
EK4702A binding inside an or or not pattern (D210)Bind outside the combinator, or match each alternative in its own arm3
EK4703An is whose pattern binds names outside the condition of an if or while (D211)Move the test into an if or while condition, or use match3
EK4704A pattern form Effeck does not support yet: regular expression, relational, or a computed key other than a name or literal (D212)Use a guard: if (/re/.test(x)) or if (x < 5)3
EK4705and, or, and not combined at one level without parentheses (D206)Add parentheses: (a and b) or c3
EK4801A statement that discards an Effect value, as program alone on a line; an error by default (D240)Call it: program() (quick fix)2
EK4802A warning: an Effect function without a contract that returns an Effect value, so its caller gets the program (D240)Call it: return program() (quick fix), or write the contract2
EK4803A warning: the program loads two versions of effect, an @effect/* package, or effeck-runtime (D240)Install one version28
EK4804A warning: a failure that is a global Error, thrown or returned by a try mapping (D240)Declare it: error Failed { message: string }3
EK4805A warning: a class that extends Error (D240)Write an error declaration3
EK4806A warning: a try mapping that returns unknown or any (D240)Return an error declaration's instance3
EK4807A warning: Layer.mergeAll of a layer that needs a service another argument provides (D240)Write wire (quick fix)4
EK4808A warning: a provide block that holds only another provide block (D240)provide A, B { … }, or provide wire(A, B) { … } when one needs the other (quick fix)4
EK4809A warning: a service whose every Effect member needs the same service (D240)Read it where the layer is built, or mark it @effeck leakable4
EK4810A warning: an assertion from one Effect type to another that drops failures or services (D240)Remove the assertion (quick fix)14
EK4811Off by default: a throw of any or unknown, or a call that adds one to E or R (D240)Throw an error declaration's instance; give the callee a contract3
EK4812A warning: new Promise in Effeck code (D240)callback((resume) => …), or sync(promise)10
EK4813An Effect 3 name of the Effect module, such as Effect.catchAll; it replaces TypeScript's TS2339 (D240)Write the Effect 4 name, Effect.catch (quick fix, also for TS2339 in .ts files)11
EK4814A constructor in a class that extends a Schema class, which decoding does not call (D240)Add a static function that makes an instance11
EK4815A warning: a throw of an Effect value (D240)Run it: program()3
EK4816Off by default: a layer provided outside a function named main (D240)Provide layers in the entry module's main4
EK4817Off by default: instanceof a Schema class (D240)Schema.is(Person)(value) (quick fix)11
EK4818Off by default: JSON.parse or JSON.stringify in Effeck code (D240)Decode with Schema.fromJsonString11
EK4819In a .ts file: yield of an Effect in a generator passed to an Effect function such as Effect.gen (D242)Write yield* (quick fix)10
EK4820In a .ts file: yield* of an Effect that never succeeds, as a statement (D242)Write return yield* (quick fix)10
EK4821Off by default: Schema.Struct with a _tag: Schema.Literal(…) field (D240)Schema.TaggedStruct("Opened", { … }) (quick fix)11
EK4822Off by default: a condition whose type is not boolean (D240)Compare explicitly: if (count > 0)2
EK4823Off by default: an arrow function whose block only returns a value (D240)Write the value as the body (quick fix)2
EK4824Off by default: a pipe with no functions (D240)Remove the pipe (quick fix)5
EK4825Off by default: a pipe of a pipe, x.pipe(f).pipe(g) (D240)Pass every function to one pipe (quick fix)5

Linting

D35 assigns type and language diagnostics to effeck and reserves type-aware linting for .ts files. A stock TypeScript checker models different call semantics and cannot type-check Effeck correctly. Run effeck --noEmit for .effeck checking. effeck-oxlint-config is not shipped, and Vite Plus integration has no acceptance coverage yet. Do not install an unpublished lint-config package from the registry.

Debugging

effeck writes source maps when sourceMap is on, and the Vite plugin always serves them. Each map records original .effeck positions on the lowered AST through TypeScript emit, so any debugger that reads standard source maps maps breakpoints and paused frames to .effeck lines: VS Code's JavaScript debugger for Node.js, Bun, and Chrome targets; Chrome and Edge DevTools; and JetBrains IDEs.

Paused on breakpoint, requestApproval  checkout.effeck:22
Call stack (JavaScript)
  requestApproval            checkout.effeck:22         the running Effeck function
  runLoop                    effect/dist/internal/effect.js
  evaluate                   effect/dist/internal/effect.js

What a debugger shows follows from the frame invariant (Chapter 26):

  • One Effeck frame at a time. Compiled functions are generator functions named after their source, so the running function appears by name at its .effeck line. Its callers are frames on the fiber's _stack, not JavaScript activations, so the debugger's call stack shows Effect's run loop below it, not the Effeck call chain.
  • Regions are closures named after their function and block: profile$try, profile$catch, profile$finally, profile$using.
  • Clauses and producers. A clause is a generator function named after its operation member (approve), and a producer's body is one named after the producer (lines). A breakpoint in a clause shows the clause; the handled program's frames are inside k, a heap value, and appear again when the program resumes. A breakpoint in a producer shows the producer, and the loop body runs in the clause of the for...of that iterates it.
  • Stepping over a call that pauses continues on a later run-loop step, so the debugger steps into Effect's run loop; set a breakpoint on the next line instead, as with Effect.gen code today.
  • Logical call stacks across calls and suspensions come from spans: set tracing to "exported" or "all", and use Effect's DevTools layer (effect/devtools) with its VS Code extension, which shows fibers and the span stack.