refactor(core)!: RateLimitError extends StitchError - #662
Merged
Conversation
The two error classes were siblings, and CONTRACT.md P10 kept them in parity by having `RateLimitError` re-declare `status`/`attempts`/`body`/`url` by hand — a written rule enforcing exactly what `extends` gives for free. Root the taxonomy at `StitchError` instead. The duplication had a cost beyond tidiness. `SafeResult.error` is typed `StitchError`, so `.safe()` coerced a delegate-backoff `RateLimitError` into a bare one: the instance moved to `.cause`, the `instanceof` test stopped working, and `error.body` came back `undefined` — dropping the payload an outer gate reads to pace itself. The mode whose whole point is handing back-pressure outward lost its signal on the path the docs otherwise recommend. `.safe()` now returns the same instance `await` throws. (Independently found from the consumer side by the scenario pass in #638, `body-verdict-footguns.md` item 2.) Every dispatch site also carried a two-arm `instanceof StitchError` / `instanceof RateLimitError` check; #639 was adding a second copy of it inside one function. One test now covers both. The one hazard the subclassing introduces is arm ordering — a leading generic `StitchError` arm swallows the delegate signal. `engine.ts`'s `errEvt` already had the order right (a RateLimitError also carries `.response`); it is now commented as load-bearing, P10 requires it, and the docs and error catalog say so where they branch. Also fixes the test stub, which flattened a stubbed `RateLimitError` and never stamped `retryAfter` on the streamed `error` event the way the engine does. Breaking for consumers that branch on both classes. Pre-GA (1.0.0-rc.7). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rejifald
added a commit
that referenced
this pull request
Aug 5, 2026
Review of #639 flagged that this PR adds a second copy of the `instanceof StitchError || instanceof RateLimitError` predicate six lines below the one already in the function — the same question asked negatively (flatten a foreign error carrying `.response`) and then positively (pass an engine-minted error through). Bind it once. Behaviour is identical; the tests are unchanged and still green. The duplication is a symptom: the two classes are siblings, so every dispatch site has to name both. #662 makes `RateLimitError` extend `StitchError`, after which this collapses to a bare `source instanceof StitchError` — noted in a comment so the follow-up is obvious at the site. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 5, 2026
Open
rejifald
added a commit
that referenced
this pull request
Aug 5, 2026
…ngrades `body-verdict-footguns` finding 2 measured `.safe()` handing back a bare StitchError with `body: undefined` when a delegate-backoff RateLimitError was the terminal, with the real instance reachable only via `.cause`. #662 fixes it, and not by either of the two asks (preserve the fields across `asStitchError`, or document that `delegate` needs try/catch): the coercion existed only because the two classes were siblings while `SafeResult.error` is typed `StitchError`. Making RateLimitError extend StitchError removes the need for it — `.safe()` returns the instance `await` throws. Rewrites the (b2) proof block to measure the new behaviour (verified 19/19 against #662's core) and marks the draft + LEDGER row accordingly. Findings 1 and 3 are untouched and still open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 5, 2026
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.
What
RateLimitErrorandStitchErrorwere siblings, both extendingErrordirectly. This roots the taxonomy atStitchErrorand makesRateLimitErrora subclass.Raised while reviewing #639, which adds a second copy of the two-arm
instanceof StitchError || instanceof RateLimitErrorcheck insiderebuildError— six lines below the one already there.Why it wasn't just tidiness
CONTRACT.md P10 required the two classes to "guarantee the same field set … so a consumer can branch on any thrown error uniformly", and
RateLimitErrorsatisfied it by re-declaringstatus/attempts/body/urlby hand. A written rule enforcing exactly whatextendsgives for free — and it already failed its own goal on one path:SafeResult.erroris typedStitchError, so.safe()had to coerce a delegate-backoffRateLimitErrorinto a bare one. The instance moved to.cause,instanceof RateLimitErrorstopped working, anderror.bodycame backundefined— dropping the payload an outer gate reads to pace itself. The mode whose entire purpose is handing back-pressure outward lost its signal on the path the docs otherwise recommend (.safe()overtry/catch).That was found independently from the consumer side by the scenario pass in #638 —
docs/scenarios/issue-drafts/body-verdict-footguns.md, item 2, ".safe()downgradesRateLimitErrorand drops the body", severity medium, "an outer rate-gate backs off blind." This closes it.The one hazard, and how it's handled
Subclassing makes arm order load-bearing: a leading generic
instanceof StitchErrornow swallows the delegate signal.engine.ts'serrEvtalready had the order right (aRateLimitErroralso carries.response, so it always had to be tested first) — now commented as load-bearing.docs/errors/index.mdxand the delegate-backoff guide say it where they branch, anddocs/errors/rate-limit.mdxcarries a warning callout.errors/index.mdxcatalog example was already ordered correctly.Changed
resilience.tsRateLimitError extends StitchError; hand-mirrored P10 fields deleted,statusnarrowed tonumberviadeclare(no emit — a real field would clobber the base underuseDefineForClassFields), gainscausestitch.tsinstanceof StitchError;asStitchErrorstops downgrading, so.safe()returns the instanceawaitthrowsengine.tstest-stub.tsRateLimitErrorkeeps its identity, and the streamederrorevent now carriesretryAfterthe way the engine's doeserrors/rate-limit.mdx,errors/index.mdx, delegate-backoff guide, core README, CONTRACT P10 (parity by inheritance, with the history of why)Verification
check:types·check:types-d·check:lint·check:format·check:contract·check:docs-links·check:changelogall green; 1440 core tests pass, full workspace suite green.Two tests changed shape, both to assert the better behaviour:
.safe()now yields the realRateLimitError(was: a wrapper with it on.cause), and the public-API surface test pins the hierarchy in both directions. A new test pins the whole inherited P10 field set plusnameas the discriminator.Breaking
Yes, for consumers branching on both classes — see the CHANGELOG entry for the migration. Core is
1.0.0-rc.7, so this is the cheap window; after GA it's a major.🤖 Generated with Claude Code