refactor!(error): replace createTaggedError with defineErrors — Rust-style namespaced errors#101
Merged
refactor!(error): replace createTaggedError with defineErrors — Rust-style namespaced errors#101
Conversation
Add spec for replacing createTaggedError with defineErrors — a constructor-function-based API that eliminates the builder chain, phantom types, and mode complexity. Mark 4 prior tagged-error specs as superseded with forward references.
Create src/error/defineErrors.ts with ~12-line runtime that stamps `name` from keys and generates both plain and Err-wrapped factories. Add new types (ErrorBody, ErrorsConfig, FactoryPair, DefineErrorsReturn, InferError, InferErrorUnion) to types.ts, replacing the old TaggedError generic.
36 tests covering all function shapes: zero-arg static, call-site message, computed message, fields+message, mixed shapes, InferError/InferErrorUnion type extraction, JSON serialization, edge cases. Fix ErrorBody type to not use JsonObject intersection (breaks optional fields). Fix runtime to put name last in spread to prevent override.
BREAKING CHANGE: `createTaggedError` and `TaggedError` type are removed. Use `defineErrors` with `InferError`/`InferErrorUnion` instead. Move `extractErrorMessage` to its own file. Delete the old builder (utils.ts) and its test file (createTaggedError.test.ts).
Update 28 documentation files to use the new defineErrors API. Replace all createTaggedError builder chain examples with defineErrors constructor function pattern. Replace TaggedError<T, F> generics with InferError<typeof errors, 'Name'> or inline Readonly<...> types. Update imports, explanations, and API reference sections.
Rename 'constructor' to 'ctor' to avoid shadowing global. Replace 'Function' cast with explicit function type. Add biome-ignore comments for necessary 'any' usage in type utility types.
…-default BREAKING CHANGE: defineErrors API redesigned to use short variant names under a namespace (like Rust's thiserror) instead of keys ending in Error. - Factories now return Err<...> directly (no dual FooError/FooErr factories) - Keys are short variant names (Connection, Parse) not ConnectionError - InferError<T> takes a single factory (typeof HttpError.Connection) - InferErrors<T> replaces InferErrorUnion<T> for union extraction - ValidatedConfig provides descriptive error when 'name' key is used - All tests rewritten for v2 API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the
createTaggedErrorbuilder chain withdefineErrors— a single function that takes a config object of constructor functions and returns namespaced error factories that produceErr<...>directly.The old builder API (
createTaggedError('FooError').withFields(...).withMessage(...)) had three problems: phantom-type state tracking made the type machinery complex, the dual-mode.withMessage()design created a branching API surface nobody used, and call sites always needed an extraErr()wrap fortrySync/tryAsync. Rust'sthiserrorcrate showed a better pattern — short variant names under a namespace, one constructor shape per variant, done.The namespace provides context (
HttpError.Connection), so variant names stay short. Value and type share the same name — just likeclassdeclarations.What changed
createTaggedError,TaggedErrortype, and the builder inutils.tsare removeddefineErrors(config)is the single entry point — ~12 lines of runtimeErr<Readonly<{ name: K } & ReturnType<Fn>>>directlyInferError<typeof Ns.Variant>extracts a single error type;InferErrors<typeof Ns>extracts the unionValidatedConfigcatches accidentalnamekeys at the type level with a descriptive error messageextractErrorMessagemoved to its own file (behavior unchanged)Naming convention
The API enforces a two-level naming scheme:
Namespace name = domain +
Errorsuffix (e.g.,HttpError,RecorderError,DbError)Variant name = specific failure mode, never generic (e.g.,
Connection,AlreadyRecording,QueryFailed)Anti-patterns for variant names:
Service— too generic.RecorderError.Service({ message: '...' })forces the caller to supply meaning that the variant name should carry. UseRecorderError.AlreadyRecording()instead.Error— redundant with the namespace.DbError.Errorsays nothing.Failed— too vague on its own. Prefix with what failed:ConnectionFailed,InitFailed.Why short variant names?
HttpError.ConnectionErrorstutters.HttpError.Connectionreads naturally. Thenamefield stamps"Connection"— discrimination works on the variant, not the fully-qualified name.Why Err-by-default?
trySync/tryAsynccatch handlers are the 90% use case, and they expectErr<E>. The old API requiredErr(MyError({...}))at every call site. Now factories returnErr<...>directly. For the rare case you need a plain error object,.errorunwraps it.Different shapes, one abstraction
No modes, no builder state. Just different function signatures: