Skip to content
LogoLogo

Failures

Sources: VIP-4, VIP-13, VIP-29, VIP-41. Handling failures with catch is in Running Effects, and matching them is in Pattern matching.

throw

Meaning

Synchronous VibeLang code never throws (VIP-4, amended 2026-10-31): a function that throws is an effect function, or TypeScript.

Wherethrow e means
effect codethe Effect fails with e, a typed error: return yield* Effect.fail(e)
an async function, arrow or method, its parameter list included, an async generator's body, or an async matchJavaScript's throw: the returned Promise rejects with e
any other .vibe codea compile error, EK0401 (Plain code never throws)
a .ts or JavaScript file, or a module body (VIP-40)JavaScript's throw; callers see it through @throws (Thrown types)

A throw expression follows VIP-41 and its pinned TC39 proposal. Its syntax restriction is EK1010; Appendix A gives the grammar.

VibeLang
// compiler-gap: pending VIP-4
effect function load(id: string) {
  const user = try findUser(id)
  if (!user) throw new NotFound({ id })
  return user
}
Effect
// fragment
function load(id: string) {
  return Effect.gen(function* () {
    const user = yield* findUser(id)
    if (!user) return yield* Effect.fail(new NotFound({ id }))
    return user
  })
}

Plain code never throws

Synchronous code outside effect code is every part of a .vibe or .vibex file that is not effect code and not in an async function or async match boundary (nested source callables keep their own contexts): plain functions and arrows (one nested in effect code included), plain methods, constructors, accessors, plain generators, class field initializers, static blocks, every parameter list except an ordinary async function's, plain catch handlers (parameters and body), and the module top level. In that code:

  • A throw statement or throw expression is EK0401, even when a try statement in the same function catches it: const v = s ?? throw new Error("empty") in a plain function is EK0401 (Throw expressions).
  • A call, new expression, tagged template or class evaluation known to throw is EK0401, unless it sits in the try block of a try statement with a catch clause in the same function. Its callee has a written @throws {T} other than @throws {never} (@throws {unknown} included), or has a @rethrows parameter given a callback known to throw. A call whose throwing is unknown, such as an unannotated .ts function, stays an ordinary call (VIP-40); in effect code the same call has thrown type unknown and needs its wrapper (EK0403).
  • A plain function whose return type is wholly effectful, and an effectful function's parameter list, are stricter: any thrown type other than never is EK0401 (Effectful callables).
  • A function literal passed where the parameter's function type admits throws, as the thunks of Effect.try and Effect.tryPromise and the Promise executor do, or passed to a @rethrows parameter, may contain calls known to throw: they belong to the enclosing call. A function nested in that literal is its own plain code. A throw statement or throw expression in the literal is still EK0401.
  • TypeScript's try { } catch { } statement stays allowed.
VibeLang
// compiler-gap: pending VIP-4
function check(s: string) { if (!s) throw new Error("empty"); return s } // EK0401
const parsed = (s: string) => JSON.parse(s)                               // EK0401: JSON.parse throws SyntaxError
effect function guest() { return fail("x") catch (e) => { throw e } }   // EK0401: a plain handler
async function load(id: string) { if (!id) throw new Error("no id"); return id } // allowed: load("") rejects
function orNull(s: string) { try { return JSON.parse(s) } catch { return null } } // allowed

Quick fixes, in order: make the function effect, so throw is Effect.fail and a call known to throw runs with try Effect.try(() => …); make it effect and write return try die(e); or move the code to a .ts file. Code that can't be effect (constructors, accessors, plain generators, initializers, static blocks, parameter lists, the top level) gets only the .ts fix. A plain handler gets x catch effect (e) => { throw e } and x catch effect (e) => { return try die(e) }. An exported function of a published package offers the .ts fix first, because effect changes its declaration.

Throw expressions

VIP-41 adopts the pinned TC39 proposal exactly for grammar, precedence and parentheses. See Appendix A for the productions. A throw expression takes a unary operand: value ?? throw new Missing(), condition ? throw error : fallback, and unary nesting such as !throw error parse. A statement starting with throw is still a throw statement.

A token in ThrowExpressionInvalidPunctuator immediately after the operand is an error (EK1010). Parenthesize the operand to throw a compound value, value ?? throw (a + b), or parenthesize the throw before another operator, (throw error) + b. Commas count: f(throw error, b) is invalid; f((throw error), b) parses. The first arm of condition ? throw error : fallback needs no parentheses because : is not forbidden. Neither are the keyword operators in and instanceof, or TypeScript's as and satisfies. ECMAScript automatic semicolon insertion still applies: a line break can end an initializer before a new statement or class member.

Where a throw is allowed at all remains VIP-4's no-throw rule (EK0401). This grammar does not authorize a plain function to throw or change eager parameter-initialization rules. Throw expressions remain supported; no migration to do { throw (operand); } is required.

VIP-7's direct match-arm body has its separate "throw" Expression production: default: throw a + b; throws the full operand, as a throw statement does. A throw expression nested within an arm still follows VIP-41.

In effect code a throw expression fails the Effect, exactly as the corresponding throw statement does (VIP-41). Its counterpart is yield* Effect.fail(e) in the same expression position, with type never:

VibeLang
// compiler-gap: pending VIP-41
error Missing
effect function required(value: string | undefined) {
  const v = value ?? throw new Missing()
  return v
}
Effect
import { Data, Effect } from "effect"
 
class Missing extends Data.TaggedError("Missing") {}
function required(value: string | undefined) {
  return Effect.gen(function* () {
    const v = value ?? (yield* Effect.fail(new Missing()))
    return v
  })
}

required("a") gives Success("a"); required(undefined) gives Failure(Missing).

Thrown types

This section is the one definition of throw tracking; every other chapter refers to it. A callable declared outside VibeLang, in a .ts, .js or .d.ts file, an npm package or a module body (VIP-40), carries a thrown type by one rule, as TypeScript's own (VIP-4, amended 2026-11-02):

DeclarationThrown type at a call
/** @throws {T} */ on it or its call signatureT, trusted, not checked
no tagunknown
/** @rethrows p */its own thrown type, plus the thrown type of the callback passed for p; @throws {never} does not erase it

A callable declared in VibeLang (.vibe or .vibex, outside a module body) has thrown type never. Nothing is inferred from a body. A written @throws or @rethrows tag there, on a function, service member or function-type signature, is EK0406: remove it and declare failures in the Effect's error type. Declaration output records @throws {never} on non-effectful signatures, not an inferred contract (Declarations); a re-exported foreign callable keeps its contract.

Tags attach to call signatures: each overload and a returned callable's own signature carry their own. const g = f preserves f's contract; a tag on an alias of a function type does not attach, so in a foreign declaration write type Reader = { /** @throws {E} */ (): string }. Calls through any or Function throw unknown; callable unions combine their alternatives. A generic signature is classified as effectful before type-argument substitution, through its constraints. Getters and setters are not tracked calls.

Effectful callables

A callable is effectful when its return type, written or inferred, is an Effect, a Stream or a Layer: an effect function or producer, Effect's own sleep, sync, tryPromise and Effect.try, a service member typed as an Effect, a .ts function whose return type is Effect. Except for never, any and unknown (below), ‘an Effect’ uses the Running Effects test: assignability to Effect.Effect<any, any, any>; the other tests are assignability to Stream.Stream<any, any, any> or Layer.Layer<any, any, any>. For example, in Effect 4.1.1 this includes Exit values, Config programs, error instances, service keys and references; this is not an exhaustive list. A plain function whose return type is wholly one of these Effect subtypes is effectful and may not throw: function bad(): Exit.Exit<number> { throw "x" } is EK0401. Stream and Layer have their separate effectful-return categories above; neither they nor Fiber pass the Effect execution operand test. An effectful callable's thrown type is never, and it never rethrows a callback: its failures are its Effect's error channel. This is a static contract, not a JavaScript runtime guarantee: a .ts implementation that throws before returning produces a defect when evaluated inside an Effect generator. Evaluated in a parameter default, before the Effect exists, it throws a JavaScript exception at the call of a plain function, and makes the call of an effect function return an Effect that dies with the thrown value, as an async function's call returns a rejected Promise (Functions, Code outside the generator).

never, any, unknown and unions with a non-effectful member, such as Effect.Effect<number> | undefined, are not effectful, so they keep the rules above. A .ts function that returns an Effect and throws before returning is a defect inside an Effect generator, as in Effect TypeScript.

An ordinary nongenerator async callable declared in .vibe has synchronous thrown type never: its body, parameter/default initialization and callback throws reject its Promise. This applies to declarations, methods and arrows, but not async generators. Its callback parameters follow their written callable signatures; a source @rethrows tag is EK0406, as on any other .vibe callable. Declaration output carries callee @throws {never}. A returned callable keeps its own thrown contract. A foreign Promise-returning plain function can instead throw synchronously before returning; its ordinary own-throws and rethrows contracts remain. A Promise return type alone does not establish the async rule. Promise construction separately catches executor throws, as specified below. Constructing an error or data variant throws never; this is the intrinsic static contract. The Effect 4.1.1 constructors still read and copy the payload fields, including ordinary getters, Proxy traps and target property operations (Object.assign, CopyDataProperties). Exceptions from those implicit operations retain their native order and value and follow Implicit throws: they are defects in effect code, even when the payload is well-typed. An Effect API overload returning an Effect is effectful. A data-last overload such as map(f) returns a function: it is non-effectful, with @throws {never} only when its signature carries that annotation; otherwise its thrown type is unknown. The returned callable has its own signature contract; the shipped rethrowing array methods are Array.prototype methods and Array.from, not those Effect globals. The runtime import() operation's own synchronous thrown type is never: its failures reject the Promise. Independently evaluated source argument expressions keep their own contracts and left-to-right order; a throwing makeSpecifier() can throw before the operation creates a Promise. Type-only import(...) queries are not runtime import expressions.

The compiler supplies intrinsic annotations for TypeScript's lib globals and the explicit wrapper thunks (Appendix E). An npm signature uses a library row below only when it carries that annotation; the inventory grants no implicit package contract. For example, an unannotated data-last retry(p) returns a function, so its thrown type is unknown, despite only building that function. Use an explicit Effect.try bridge or a written signature contract.

Call in effect codeSynchronous thrown typeUse
JSON.parse(s)SyntaxErrortry Effect.try(() => JSON.parse(s))
Math.max(1, 2)nevercall directly
Option.some(1) with a written @throws {never} contractnevercall directly
Schema.decodeUnknownSync(Schema.String) or Schema.decodeSync(Schema.String) with a written @throws {never} contractnevercall directly to build the decoder
decode(value) for that decoderSchemaErrortry Effect.try(() => decode(value))

Implicit throws. A throw from receiver or environment state is implicit: a frozen or non-extensible receiver, a revoked or trapping proxy, a user getter, setter, valueOf, toString or Symbol.toPrimitive, a detached buffer, a resource limit or stack overflow, reading a property of undefined, an iterable's iterator, a no-match TypeError, a pattern protocol error. It is never tracked, and it is a defect in effect code, as in Effect.sync. Every other throw a lib global specifies is argument validation, tracked with its class when reachable from any input admitted by the selected signature's receiver and parameter types under TypeScript strict. A safe literal does not erase it: "x".repeat(3) keeps RangeError. A throw no well-typed argument reaches is not tracked: Object.keys(o) with o: object throws never, and padStart and padEnd use ToLength, which clamps instead of validating, so padding's own thrown type is never.

Effect
import { Effect } from "effect"
 
const keys = (o: object) => Object.keys(o) // throws never for well-typed arguments
const builtins = Effect.gen(function* () {
  const names = keys({ a: 1 })
  const padded = "x".padEnd(-1, "0") // no tracked count-validation throw
  const fixed = yield* Effect.try(() => (1.5).toFixed(2)) // the signature still includes RangeError
  const repeated = yield* Effect.try(() => "x".repeat(3)) // a safe literal keeps that contract
  return [names, padded, fixed, repeated] as const
  // Success([["a"], "x", "1.50", "xxx"]); repeat(-1) and toFixed(101) fail through Effect.try.
})
VibeLang
// compiler-gap: pending VIP-4
effect function f(xs: number[], s: string, v: object) {
  xs.push(1)                                          // implicit: a frozen receiver is receiver state
  const n = s.length                                  // no throw
  const t = JSON.stringify(v)                         // EK0403: may throw TypeError (cycle, bigint)
  const u = try Effect.try(() => JSON.stringify(v))   // ok
  return "ab".repeat(n)                               // EK0403: may throw RangeError
}

The normative throws.json supplies the remaining intrinsic global contracts and records proposed npm signature contracts, generated by node docs/tools/throws-inventory.mjs --write from this repository's bundled TypeScript lib.*.d.ts files and effect@4.1.1 declarations and dist sources (VIP-4). It records each module, declaration and overload signature, own throws, @rethrows parameters, callback admission and returned callable contracts, with exact versioned inputs and their hashes. For npm signatures this inventory is informational: only a written signature annotation or the ordinary effectful-return rule grants a contract.

xs.push(x), new Map() and Object.keys(o) have own thrown type never; JSON.stringify(v) has TypeError and s.repeat(n) has RangeError. An explicit mapping callback of Array.from rethrows. Calls written in a match subject, guard or arm follow the ordinary rules.

A generated schema error constructor has thrown type never, a static contract. Effect 4.1.1 validates its fields; invalid input can throw a plain Error, which becomes a defect inside Effect.gen.

VibeLang
// compiler-gap: pending VIP-29
schema error Invalid { name: Schema.NonEmptyString }
effect function invalid() { throw new Invalid({ name: "" }) } // defect, not typed failure
Effect
import { Effect, Schema } from "effect"
 
class Invalid extends Schema.TaggedError<Invalid>()("Invalid", { name: Schema.NonEmptyString }) {}
const invalid = Effect.gen(function* () {
  return yield* Effect.fail(new Invalid({ name: "" }))
})

Assignability

A callable whose thrown type is T is assignable to a function type only when that type admits T: a type without @throws admits only never, @throws {U} admits subtypes of U, @throws {unknown} admits anything, and a parameter named by @rethrows admits anything. So with a foreign parse, const r: () => string = parse is EK0402: wrap it, or declare the annotated function type in a .ts file or module body and import it. The rule applies wherever TypeScript checks one call signature against another: assignments, arguments, returns against a written return type, object members, class overrides and implements, with service members as interface members.

Effect.try and Effect.tryPromise declare their thunk @throws {unknown} in both overloads, so Effect.try(() => parse(s)) is allowed and the wrapper turns a throw into a failure. Effect's LazyArg and other callback parameter types, those in .d.ts files included, describe what an argument callback may throw: without @throws, they admit only never. So sync(() => parse(s)) is an error. The Promise executor also admits @throws {unknown}, and the constructor itself throws never.

Calls in effect code

RuleDiagnostic
In effect code, a call, new expression, tagged template or class evaluation whose thrown type is not never. Wrap it: try Effect.try(() => parse(s))EK0403
In effect code, a call (runtime import() included), new expression, tagged template or async match producing a Promise or thenable, even if assigned to a variable and never awaited. Move the expression into a tryPromise thunk, whether or not it can reject; promise also works when the thunk's thrown type is neverEK0404

EK0404 uses the Promise test of Result tests: never, any and unknown never trigger it; any union with at least one PromiseLike member does. For a tagged template, apply this test to the selected tag signature's result. Keep the original native invocation inside the bridge thunk, preserving its receiver, cached template object identity, once-only substitutions and evaluation order.

VibeLang
// compiler-gap: pending VIP-4
async function tag(_parts: TemplateStringsArray) { return 7 }
effect function unwrappedTag() { const p = tag`x`; return 0 } // EK0404
effect function wrappedTag() { return try tryPromise(() => tag`x`) } // Success(7)
Effect
import { Effect } from "effect"
 
async function tag(_parts: TemplateStringsArray) { return 7 }
function wrappedTag() {
  return Effect.gen(function* () {
    return yield* Effect.tryPromise(() => tag`x`)
  })
}
// Success(7).
VibeLang
// compiler-gap: pending VIP-4
function maybeQuery(): Promise<number> | undefined { return undefined }
effect function mixed() {
  // const p = maybeQuery() // EK0404: may produce a Promise
  return try tryPromise(async () => maybeQuery())
}
Effect
import { Effect } from "effect"
 
function maybeQuery(): Promise<number> | undefined { return undefined }
const mixed = Effect.tryPromise(async () => maybeQuery())

Effect code calls a function whose thrown type is never directly. Promise-producing calls inside wrapper thunks are ordinary JavaScript; wrapping never exempts a platform call from VIP-33. Promise.reject(e), like every Promise-producing call, must be inside a wrapper thunk in effect code. An unannotated foreign queryDb may throw unknown, so use tryPromise(() => queryDb(q)); promise does not admit that throwing thunk.

A dynamic import uses the same explicit Promise bridge. Enclose the complete native import expression, including its arguments, in the thunk once per execution; bridging never admits a forbidden literal module specifier. The local worker.mjs fixture exports const value = 7 with matching declarations.

VibeLang
// compiler-gap: pending VIP-4
effect function unwrappedImport() { const module = import("./worker.mjs"); return 0 } // EK0404
effect function importedValue() {
  const module = try tryPromise(() => import("./worker.mjs"))
  return module.value
} // Success(7)
type Worker = typeof import("./worker.mjs") // erased type query
Effect
// fragment: ./worker.mjs exports const value = 7 and has matching declarations
import { Effect } from "effect"
 
function importedValue() {
  return Effect.gen(function* () {
    const module = yield* Effect.tryPromise(() => import("./worker.mjs"))
    return module.value
  })
}
type Worker = typeof import("./worker.mjs")
// importedValue succeeds with 7; Worker is a type query, not a runtime import.
export { importedValue }

A call known to throw in a plain catch handler's parameters or body is EK0401. Use an effect handler and bridge the call in its body:

VibeLang
// compiler-gap: pending VIP-4, VIP-5
// fragment: source: string!Error; parse(e: Error): string is imported with a written @throws tag
source catch (e) => parse(e) // EK0401
source catch effect (e) => try Effect.try(() => parse(e))
Effect
// fragment
const source = Effect.fail(new Error("input"))
function parse(e: Error): string { throw e }
const recovered = Effect.catch(source, (e) => Effect.try(() => parse(e)))
VibeLang
// compiler-gap: pending VIP-4
error BadInput
effect function parsed(s: string) {
  return try Effect.try({ try: () => parse(s), catch: () => new BadInput() })
}
Effect
// fragment
class BadInput extends Data.TaggedError("BadInput") {}
function parsed(s: string) {
  return Effect.gen(function* () {
    return yield* Effect.try({ try: () => parse(s), catch: () => new BadInput() })
  })
}

parsed fails with BadInput when parse throws; the thunk-only form fails with Effect's UnknownError.

Effect.try keeps its module prefix because try is a reserved word (VIP-6).

Code outside the generator

try works like await (VIP-5): parameter defaults, field initializers and static blocks are not effect code (Appendix A). They keep JavaScript's timing, and they are plain code under Plain code never throws:

  • Parameter defaults and destructuring run at the call, before the Effect exists. In an effectful function's parameter list a default whose thrown type is not never is EK0401, and in any other parameter list outside an async function a default known to throw is. A default that returns a Promise is a plain value. Destructuring can still throw an implicit TypeError: the call of an effect function then gives an Effect that dies with that value, and a producer's gives a Stream that dies with it, as an async function rejects. The call itself never throws.
  • An ordinary async parameter list rejects: a throwing default rejects the returned Promise, and an outward return v in a default (x = do { return 7 }) skips the body and fulfills the Promise with v.
  • Field initializers and static blocks run in new X() and at class evaluation. Throw checks follow Thrown types and Plain code never throws, with each expression's source context and the native caught-call exemption preserved.
  • Arguments are evaluated by the caller. Wrapper thunks run inside the Effect.
VibeLang
// compiler-gap: pending VIP-4
// fragment: s: string and q: string are supplied by the enclosing scope
effect function a(p = parse(s)) { return p }          // EK0401: a default known to throw
async function resolved() { return 1 }
effect function b2(p = resolved()) { return p }      // allowed: p is a Promise
effect function c({ id }: User) { return id }         // c() from untyped JS gives an Effect that dies with TypeError

error declarations

Syntax

ErrorDeclaration, with an ErrorBody of Fields and methods: ClassMethod (ordinary TypeScript class methods, including plain and async overload signatures, generator and async generator implementations, getters, setters and private names), with TypeScript's class modifiers, decorators, overload ordering, adjacent-implementation and compatibility checks, plus effect methods and producer methods, as any class can have (VIP-13). A setter parses as a ClassMethod and the static rules below reject it. Field and Separator are shared with schema and data, so adjacent fields on one line need a separator. The field type is parsed greedily as a TypeScript type member: a line break separates members only after the type is complete. An explicit readonly is allowed and redundant. Empty ; class elements between members are ignored, as in TypeScript. Two members on one line with no separator, error E { a: number b: string }, are EK1306; the quick fix inserts ;.

error, like service, schema and data, follows TypeScript's contextual keyword type: a declaration is the keyword, no line break, then the declaration's name (a BindingIdentifier), where a declaration can start; anything else is ordinary TypeScript, where error is an identifier (Declaration keywords, the one rule for all four). So const error = …, catch (error), const e = error as Error and error instanceof NotFound still work, and error as T; declares as, then fails; write (error as T);. The body is optional, as for TypeScript's declare module "m" (ErrorDeclaration). As in ECMAScript's ExportDeclaration, export error E and export schema error E are named declarations; export default error E and export default schema error E are syntax errors (EK1305). Write export { E as default } instead, as for a service.

Bodyless plain and async overload signatures are methods in both error forms, as in a TypeScript class; they execute no code themselves. For example, labels() gives an Effect with Success(["ok", 7]):

VibeLang
// compiler-gap: pending VIP-13, VIP-29
error Plain {
  label(x: string): string;
  label(x: number): number;
  label(x: string | number) { return x }
}
schema error Encoded {
  label(x: string): string;
  label(x: number): number;
  label(x: string | number) { return x }
}
effect function labels() { return [new Plain().label("ok"), new Encoded().label(7)] }
Effect
// Error-method overload counterpart
import { Data, Effect, Schema } from "effect"
class Plain extends Data.TaggedError("Plain") {
  label(x: string): string;
  label(x: number): number;
  label(x: string | number) { return x }
}
class Encoded extends Schema.TaggedError<Encoded>()("Encoded", {}) {
  label(x: string): string;
  label(x: number): number;
  label(x: string | number) { return x }
}
const labels = () => Effect.gen(function* () {
  return [new Plain().label("ok"), new Encoded().label(7)]
})
console.log(JSON.stringify(Effect.runSync(labels())))

Each member has one classification (Declarations): id: string, readonly id: string and id?: string are always Fields, each complete method or accessor is classified once, including effect and producer methods, empty ; class elements are ignored, and any other class element is a RejectedElement, which the static rules below reject. The parser also accepts type parameters and heritage clauses, only so their rules can report them.

Field keys follow their actual counterpart's native checks. In a plain error, fields are type-only payload members whose keys use TypeScript's TypeLiteral member admission, matching Data.TaggedError's payload type. For example, with keys.value: number and key(): number, [keys.value] is admitted, while [(keys.value)], [keys["value"]] and [key()] retain native diagnostics such as TS1170, even for a call returning a literal. Admission depends on the expression shape as well as its type. Admitted computed keys and any resulting index signature are erased, with no runtime key evaluation or TDZ read. Actual methods and getters keep TypeScript's computed-name evaluation.

schema error field keys instead use the runtime object literal admission of Schema.TaggedError's fields object (Schema declarations), together with the existing _tag exclusion checks. The numeric call, parenthesized and element access forms above can be admitted in that counterpart. An admitted computed field key runs while building the schema-fields object and supplies just its one runtime key. The decoder requires that key and ignores other keys by default; an index signature does not make it validate arbitrary keys.

A noncomputed __proto__ field is an own data field, including quoted and escaped spellings and fields in nested structs. Emit ["__proto__"] in the generated schema-fields object so JavaScript's prototype-setting syntax cannot remove the field from validation.

In a schema error body, a computed key is EK2117 unless its well-typed runtime property-key domain is proved to exclude "_tag". Raw assignability of the unbranded string literal to a nominal key type does not establish that exclusion. Resolve effective generic constraints recursively; broad string constraints and unions that may contain the tag are rejected. Use enum members' underlying string values. In intersections retain genuine primitive literal/template constraints, but an opaque brand or refinement marker does not exclude the underlying string. Every possible union component must exclude it.

Known numeric and symbol domains, including unique symbols, cannot become that string key. Safe literals, literal unions, templates and their brands remain admitted; never has no possible key. Reject any, unknown, unresolved/cyclic domains or any other unprovable exclusion conservatively. Ordinary TypeScript invalid-key errors remain independent. Use a literal key or a symbol when needed. Accepted keys keep their existing evaluation order and are emitted as written: no decoder/refinement is executed to prove exclusion, no value is unwrapped and no runtime tag guard is added.

VibeLang
// fragment
// compiler-gap: pending VIP-29
enum Field { Tag = "_tag", Count = "count" }
declare const field: Field
schema error Bad { [field]: number } // reserved-tag diagnostic: Field.Tag remains possible
schema error Good { [Field.Count]: number }
function generic<K extends string>(key: K) {
  schema error AlsoBad { [key]: number } // the string constraint may be "_tag"
}
Effect
import { Schema } from "effect"
enum Field { Tag = "_tag", Count = "count" }
class Good extends Schema.TaggedError<Good>()("Good", { [Field.Count]: Schema.Number }) {}
const admittedKeyExit = Schema.decodeUnknownExit(Good)({ _tag: "Good", count: 1 }) // Success
const BrandedKey = Schema.String.pipe(Schema.brand("Key"))
const brandedKey = Schema.decodeUnknownSync(BrandedKey)("_tag")
const rawBrandAssignable: "_tag" extends typeof brandedKey ? true : false = false
const rawEnumAssignable: "_tag" extends Field ? true : false = false
brandedKey // "_tag": nominal typing does not change the runtime property key
Effect
import { Schema } from "effect"
 
const field = "actual"
class Dynamic extends Schema.TaggedError<Dynamic>()("Dynamic", { [field]: Schema.Number }) {}
const decode = Schema.decodeUnknownExit(Dynamic)
decode({ _tag: "Dynamic", other: 1 })             // Failure: actual is missing
decode({ _tag: "Dynamic", actual: 1, other: 2 })  // Success: other is stripped
VibeLang
// compiler-gap: pending VIP-13
function plainKey() {
  error Plain { [field]: number }
  const field = "actual"
  return new Plain({ actual: 7 }).actual
}
effect function inspectKey() { return plainKey() } // Success(7); no early read of field
Effect
import { Data, Effect, Schema } from "effect"
 
// Payload projection: the type's computed key is erased before execution.
const erasedPayloadKey = Effect.sync(() => {
  class Plain extends Data.TaggedError("Plain")<{ readonly [field]: number }> {}
  const field = "actual"
  return new Plain({ actual: 7 }).actual
})
const evaluatedSchemaKey = Effect.sync(() => {
  let reads = 0
  const key = { get field(): "actual" { reads++; return "actual" } }
  class Dynamic extends Schema.TaggedError<Dynamic>()("Dynamic", { [key.field]: Schema.Number }) {}
  return [reads, new Dynamic({ actual: 7 }).actual] as const
})
// erasedPayloadKey succeeds with 7; evaluatedSchemaKey succeeds with [1, 7].
VibeLang
// compiler-gap: pending VIP-13
error Access {
  role:
    | "admin"
    | "member"
  get message() { return this.role }; // empty class element is allowed
}

Static rules

RuleDiagnostic
a constructor in an error or schema error bodyEK1301
a _tag member in an error or schema error body, or a computed schema error key whose runtime domain cannot be proved to exclude "_tag"; use a literal key or a symbolEK2117
an extends clause, error UserNotFound extends NotFound { … }, in either error formEK2117
a user-written static member or setter, in either error formEK1302
type parameters, such as error Invalid<T> { value: T }, in either error formEK1303
any other member that is not a Field, a method, a getter or an empty ; class element, in either error form (for example id = 1, private id: string, #id = 1, declare id: string, accessor id = 1, or an index signature); an implements clauseEK1304
two members on one line with no separator, in either error form; write ; or a line break between them (quick fix: insert ;)EK1306
a recognized error or schema error declaration in an ambient source context, explicit or inherited, as for a service: declare error Missing; it comes ahead of the declaration's other diagnosticsEK1307

A member rejected by Functions for effect on a getter, setter or constructor gets the applicable Functions diagnostics and no redundant error-body category diagnostic, including in either error form. This includes both EK0104 and EK0105 for async effect get x() { }. Check the ambient-context rule before classifying members. Otherwise a member gets one diagnostic: the first row that names it. The general member rule, EK1304, applies only when no specific member rule applies. For example, _tag = 1 reports EK2117; static x = 1 reports the static-member rule, not the general member rule.

VibeLang
// compiler-gap: pending VIP-1, VIP-13
error BadMember { async effect get value() { return 1 } } // EK0104 and EK0105; no extra EK1304

Meaning

error Name { fields } declares exactly Effect's Data.TaggedError class. The tag is always the name, and every field is readonly. Optional id?: T maps to readonly id?: T, as in a TypeScript type literal. Fields, methods and getters may be interleaved, as in a TypeScript class.

User-written static members and setters are forbidden in both forms. The compiler-supplied matcher below is the exception; its Symbol.customMatcher shim is described in Pattern matching.

VibeLang
// compiler-gap: pending VIP-13
error NotFound {
  id: string
  get message() { return `no user ${this.id}` }
}
error Timeout
Effect
// fragment
class NotFound extends Data.TaggedError("NotFound")<{ readonly id: string }> {
  get message() { return `no user ${this.id}` }
  static [Symbol.customMatcher](subject: unknown) {          // VIP-7: tests _tag
    return (subject as { _tag?: unknown } | null)?._tag === "NotFound" ? [subject] : false
  }
}
class Timeout extends Data.TaggedError("Timeout") {
  static [Symbol.customMatcher](subject: unknown) {
    return (subject as { _tag?: unknown } | null)?._tag === "Timeout" ? [subject] : false
  }
}

The matcher gives [subject] on a match, so when NotFound(let e): binds the error, and false otherwise. when NotFound: is a bare test; when NotFound(): never matches because its zero patterns cannot match the singleton extraction list. By comparison, when Option.None(): matches: its extracted list is empty. The 34 retained Effect error constructors, like NotFound, extract one subject, so their zero-argument extractors never match, and VibeLang reports a lint warning on such an extractor (EK0703). An error is constructed with its fields as one object, new NotFound({ id }). Two packages that both declare error NotFound match each other in match, as with Effect's catchTag, because an error class's matcher tests _tag (Pattern matching).

VIP-7 retains module-guard checks for Effect's own tagged errors, without patching Effect. For example, when Cause.TimeoutError(let e): tests Cause.isTimeoutError(subject) and binds subject as e. A bare { _tag: "TimeoutError" } does not pass that guard. These checks use neither a universal _tag test nor instanceof. The 34 resolved constructors, alias and shadowing rules and singleton extraction are listed in Retained fixed patterns.

Effect
import { Cause } from "effect"
 
function timeoutPattern(subject: unknown) {
  return Cause.isTimeoutError(subject) ? [subject] : false
}
timeoutPattern(new Cause.TimeoutError()) // [subject]
timeoutPattern({ _tag: "TimeoutError" }) // false

An error always extends Data.TaggedError. Group errors with a union, type LookupError = NotFound | Timeout: tagged errors are matched by _tag, and a subclass would match two tags.

Neither form accepts type parameters. For example, error Invalid<T> { value: T } and schema error Invalid<T> { value: T } are compile errors.

schema error declarations

Syntax

SchemaErrorDeclaration. Its ErrorBody is shared with error. Each field's type is mapped as in a schema declaration, optional properties included. Every field is readonly, as in error, so no field gets Schema.mutableKey. Omitting the body means no fields, as for error Timeout, and the body boundary is the same as for error.

schema error NotFound { and a schema named error (schema error = …) differ at the next token. schema error { id: string } is a schema named error: { is not the name required after schema error.

Meaning

schema error Name { fields } combines error and the field mapping of schema declarations (Data). Fields are written as TypeScript types and mapped to Schemas. Encoding follows those codecs' Encoded representations; JSON, worker messages and RPC transports must support that representation. The declaration adds no implicit wire conversion. For JSON, choose compatible field codecs, such as Schema.BigIntFromString for a bigint encoded as a string. The body rules, and their diagnostics, are those of error declarations. Referenced field schemas also obey the decoded-key modifier checks: every payload field is effectively readonly, even without a written readonly, so a mutable-key reference is EK2109. Nested mutable values remain mutable.

VibeLang
// compiler-gap: pending VIP-29
schema error NotFound { id: string }
schema error Timeout
Effect
// fragment
class NotFound extends Schema.TaggedError<NotFound>()("NotFound", { id: Schema.String }) {
  static [Symbol.customMatcher](subject: unknown) {          // as for error
    return (subject as { _tag?: unknown } | null)?._tag === "NotFound" ? [subject] : false
  }
}
class Timeout extends Schema.TaggedError<Timeout>()("Timeout", {}) {
  static [Symbol.customMatcher](subject: unknown) {
    return (subject as { _tag?: unknown } | null)?._tag === "Timeout" ? [subject] : false
  }
}