Skip to content
LogoLogo

VIP-13: error declarations

Status: accepted by Will, 2026-10-28.

Decision

// VibeLang
error NotFound { id: string }
error Timeout                                   // an error with no fields
 
// exactly this Effect TS
class NotFound extends Data.TaggedError("NotFound")<{ readonly id: string }> {}
class Timeout extends Data.TaggedError("Timeout") {}
 
// using one (VIP-4, VIP-5, VIP-7)
throw new NotFound({ id })                      // in an effect function: fails with NotFound
fetchUser(id) catch (e) => match (e) { when NotFound: guest; }
  • error Name { fields } declares exactly Effect's Data.TaggedError class. The tag is always the name, and every field is readonly.
  • Amended 2026-10-30: payload key erasure. Plain error payload fields are Data.TaggedError's generic type argument. Their computed type keys cause no runtime lookup, evaluation or TDZ read; declaration output retains the type. Admitted schema error field keys belong to the runtime schema object and evaluate normally, while actual methods/getters retain TypeScript's computed-name evaluation. A shared runtime-key rule never makes a plain payload type into an object initializer. See Error declarations.
  • The body may hold methods and getters, as any class can, for example get message() { return \no user ${this.id}` }. It may not hold a constructor or a _tag` override.
  • Amended 2026-10-29: no extends. An error declaration always extends Data.TaggedError, so error UserNotFound extends NotFound { … } is a compile error. Errors are grouped with a union, as in type LookupError = NotFound | Timeout, because tagged errors are matched by _tag and a subclass would match two tags. Recorded under Will's standing approval of recommendations.
  • The base is Data.TaggedError, which can't be decoded from JSON. Serializable, Schema-based errors are left to the schema VIP.
  • Two packages that both declare error NotFound match each other, as with Effect's catchTag, because VIP-7's matcher tests _tag.
  • Ambiguity: none. error is a contextual keyword on the same line as its name, so const error = … and catch (error) still work.
  • Amended 2026-10-29: no user-written static members or setters in error or schema error bodies, for now. Sent to Will on Telegram 2026-10-29 11:28 under his standing rule 'assume I accept'; no objection by 12:00.
  • Amended 2026-10-29: no type parameters. error Invalid<T> { value: T } is a compile error, for now. Sent to Will on Telegram 2026-10-29 11:28 under his standing rule 'assume I accept'; no objection by 12:00.
  • Amended 2026-10-29: exactly VIP-13. error declarations allow no extends and no _tag override; EK2117 enforces it. Sent to Will on Telegram 2026-10-29 12:28 under his standing rule 'assume I accept'; no objection by 13:00.

Corrected 2026-10-29: the example ends every match arm with ;, the last one included, as the pinned TC39 grammar requires; a newline is not a separator (found by the round-3 review).

  • Amended 2026-10-30: computed payload key admission. Plain error payload fields use the native TypeScript TypeLiteral member checks of Data.TaggedError's payload type. Expression shape matters as well as key type: an identifier or property access of type number can be admitted while a call returning a literal, a parenthesized key or element access receives native TS1170. Only admitted payload keys are erased; actual runtime member names keep their ordinary evaluation. The runtime Schema fields object in VIP-29 has its own native key admission.

Amendment: member diagnostic priority

A getter, setter or constructor rejected for an Effect modifier receives the applicable Functions diagnostics without an additional error-body category diagnostic, for both error and schema error. In particular, async effect get x() { } receives both EK0104 and EK0105 (VIP-1). Other rejected members use the first applicable member rule; the general category rule is the fallback.

History

Every finding was about error classes combined with match exhaustiveness: round 2 M7 and round 8 M1 (majors), plus five minors, all lego-kind. VIP-7's custom matcher, which tests _tag, settles them. The declaration syntax itself drew no findings.

Prior art

Zig's error{NotFound}, Swift's enum: Error, Rust's thiserror, Effect's Data.TaggedError.

Will's words

"Ok accept. Your best judgement on the questions is correct"

(The questions: the base, Data rather than Schema; and the body, methods and getters but no constructor or _tag override.)