Skip to content

fix(eslint): identity + no-swallow error law; enhance shared NestedError#49

Merged
jglanz merged 1 commit into
masterfrom
fix/eslint-config-cleanup
Jul 21, 2026
Merged

fix(eslint): identity + no-swallow error law; enhance shared NestedError#49
jglanz merged 1 commit into
masterfrom
fix/eslint-config-cleanup

Conversation

@jglanz

@jglanz jglanz commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

What

Ports the identity + error-preservation eslint law from wire-tools-ts (the two
repos share the ban set) and enhances the @wireio/shared NestedError primitive
it depends on, then sweeps the codebase into compliance. Also corrects a stale
CLAUDE.md note.

Why (business reasoning)

Errors were being swallowed. Caught errors were rewritten into bare
new Error(...) (or restrung via `${err.message}`), which discards the
original error object — its stack trace, its structured fields, its cause
chain. In production a swallowed cause turns a precise, localized failure into a
confusing far-away symptom, and the diagnosis cost is paid over and over. The fix
is to ALWAYS preserve the caught error as a cause.

Identity arrows re-implement a library primitive. x => x IS lodash
identity — hand-writing it adds an anonymous closure that reads as if it might
do something and isn't greppable.

Both are now mechanically enforced so they can't regress, matching the law
already live in wire-tools-ts.

Changes

@wireio/shared NestedError — the vehicle

  • Takes a new message AND preserves the full cause chain: the real Error
    objects survive in .causes and the native ES2022 Error.cause, so each
    cause keeps its own stack trace and message
    . A nested-exception Caused by:
    section per cause is appended to .stack, so the whole chain is visible
    wherever the error prints — not only via the programmatic accessors.
  • Structured .context (the failing input, ids, amounts) — both a property and
    folded (bigint-/cycle-safe, truncated) into the message.
  • Backward-safe: NestedError had zero consumers, so the API change breaks
    nothing. + unit tests.

eslint enforcement (eslint.config.mjs)

  • preserve-caught-error ON (was deliberately off) — a catch that rethrows
    without { cause } is an error.
  • BanErrorMessageRestring — a caught error's .message/.stack (or
    toMessage(err)) interpolated into a new Error.
  • BanBareErrorInHandler — a bare (no-cause) native Error inside an error
    handler that RECEIVES the error: Either.mapLeft/ifLeft/recoverWith,
    Promise.catch, Future.onFailure (NOT Option.orElse/ifNone — those
    handle ABSENCE; a None is not an error to preserve).
  • wire-local/no-identity-arrow — a small custom rule (esquery cannot assert
    body-name === param-name) flagging x => x.

Sweep — the rules applied everywhere

  • Identity arrows → lodash identity (api/Client, serializer/Builtins via the
    typed identity<T> so the serializer's exact signature is preserved).
  • Caught-error rewrites → NestedError({ cause }) in api/Client and
    serializer/Decoder.
  • The pre-existing BanNullUnionReturn / BanInlineTypeLiteral violations that
    the now-enforced lint surfaces in the new sysio.authex / sysio.reserv code —
    they slipped in because ci.yaml runs test:ci, not lint. Dropped the
    unenforced | null return unions (strictNullChecks is off, so the union is
    dead ceremony) and named the inline object types (SignCreateLinkOptions,
    Stringifiable).

Docs

  • CLAUDE.md: corrected the stale "No ESLint configured" line — eslint IS
    configured (copied from wire-tools-ts, same ban set + ratchet discipline).

Verification

  • pnpm build clean; eslint .0 errors (only pre-existing
    no-explicit-any warnings remain); tests green — NestedError 11, sdk-core 70
    (incl. authex/reserv/serializer/Client/api).

🤖 Generated with Claude Code

Ports the identity + error-preservation eslint law from wire-tools-ts (the two
repos share the ban set) and enhances the shared error primitive it depends on:

- @wireio/shared NestedError: takes a new message AND preserves the full cause
  chain — the real Error objects survive in `.causes` and native `Error.cause`
  (each cause keeps its own stack trace + message), a nested-exception
  `Caused by:` section per cause is appended to `.stack`, and structured
  `context` is folded into the message. + unit tests.
- eslint.config.mjs: `preserve-caught-error` ON (was off); `BanErrorMessageRestring`
  + `BanBareErrorInHandler` (a bare Error in an error handler that RECEIVES the
  error — Either.mapLeft/ifLeft/recoverWith, Promise.catch, Future.onFailure —
  NOT Option.orElse/ifNone); a `wire-local/no-identity-arrow` custom rule.
- Sweep: `x => x` identity arrows -> lodash `identity` (serializer builtins via
  `identity<T>` to keep the exact signature); caught-error rewrites ->
  `NestedError({ cause })` in `api/Client` and `serializer/Decoder`; plus the
  pre-existing `BanNullUnionReturn` / `BanInlineTypeLiteral` violations the
  now-enforced lint surfaces in the new `sysio.authex` / `sysio.reserv` code
  (`ci.yaml` runs test:ci, not lint) — dropped the unenforced `| null` return
  unions (strictNullChecks is off) and named the inline object types.
- CLAUDE.md: correct the stale "No ESLint configured" note (eslint IS configured,
  copied from wire-tools-ts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Change-Id: I50bf25b03fda4ca66f7b3a8ffc9d11f4716f08ba
@jglanz
jglanz merged commit c350950 into master Jul 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant