did-btcr2-js

ADR 085: Typed Errors Across Core Packages, Enforced by Lint

Status: Accepted

Date: 2026-07-17

Branch / PR: refactor/typed-errors

References: ADR 050 (precedent for package-scoped ESLint guard blocks)

Context

@did-btcr2/common defines a typed error hierarchy rooted at DidMethodError: every subclass carries a stable name, a machine-readable type string, and an optional structured data payload, and the constructor normalizes prototype chains and V8 stack capture. The method, aggregation, cryptosuite, keypair, and key-manager packages all build their domain errors on this hierarchy, and prior resolver hardening work added regression tests asserting that malformed input surfaces as a typed error rather than a bare TypeError.

An audit of every error construction across the workspace found the hierarchy was almost, but not quite, universal. The stragglers:

The cost of these gaps is that callers cannot uniformly catch on instanceof DidMethodError, cannot inspect .type/.data on the escaping errors, and the error contract differs from the one the rest of the codebase documents and tests.

Decision

1. Core-package sources construct only typed errors

In common, keypair, cryptosuite, key-manager, method, and aggregation, every error object constructed in src/ is a DidMethodError subclass (from common’s errors.ts or a package error module built on it), with a SCREAMING_SNAKE type string and, where useful, a structured data payload. The audit’s specific fixes:

Error messages at every touched site are unchanged, so message-matching callers and tests are unaffected; only the class, name, and type surface changed.

2. NotImplementedError joins the hierarchy

NotImplementedError now extends DidMethodError and gains the same positional constructor shape as every other subclass: (message, type = 'NotImplementedError', data?). The legacy options-object second argument is retained as a deprecated overload so the change stays within a minor version of @did-btcr2/common: common is a 9.x package on strict semver, and a major there forces a coordinated republish of every dependent (all pin ^9.x, so a lone major would leave consumer trees with two common instances and a split error hierarchy). The overload is slated for removal at the next natural common major. The one in-repo call site that used the object form (the api package’s DidMethodApi.deactivate) now passes the type string 'DID_API_METHOD_NOT_IMPLEMENTED' positionally, and that error’s name now equals its type (previously the two were set to different strings).

3. A lint rule prevents regression

A no-restricted-syntax ESLint block (following the ADR 050 precedent of package-scoped guard blocks in eslint.config.cjs) bans construction of bare Error, TypeError, RangeError, SyntaxError, EvalError, ReferenceError, and URIError in the six packages’ src/ trees. The rule matches any construction, not just throw statements, so returned and promise-rejected errors are covered too. Tests are out of scope: they may construct whatever they need.

4. Explicit exclusions

Consequences

Rejected Alternatives