Skip to content

fix(core): keep the original error as cause on FetchError - #168

Merged
birme merged 1 commit into
mainfrom
fix-fetcherror-cause
Oct 24, 2026
Merged

birme merged 1 commit into
mainfrom
fix-fetcherror-cause

Conversation

@birme

@birme birme commented Oct 24, 2026

Copy link
Copy Markdown
Member

createFetch caught non-FetchError failures (e.g. undici TypeError: fetch failed) and rethrew a FetchError with only the message, losing cause.code (ECONNRESET, UND_ERR_SOCKET). FetchError now accepts an optional cause and the catch block passes the original error.

Closes Eyevinn/osaas-ai#2476
Part of Eyevinn/osaas-ai#2462

Changes

  • packages/core/src/fetch.ts: constructor takes { message, httpCode, cause }; super(message, cause === undefined ? undefined : { cause }) (guarded, because { cause: undefined } would create an own property). Catch block passes cause: error. HTTP error paths unchanged.
  • packages/core/src/fetch.test.ts: AC1 (TypeError with ECONNRESET cause), AC2 (string rejection), AC3 (400 text, no own cause), all with await expect(...).rejects; the old error test, which passed when nothing threw, is rewritten.

Consumer impact

message and httpCode are unchanged. cause is a non-enumerable own property, so JSON.stringify(err) and enumerable-key consumers are unchanged. Grepped FetchError in packages cli, core, web and transcode (packages/*/src); packages/web/src/mypage.ts left alone as specified. A consumer outside this repo would not be found by that search.

Lessons

new Error('x', { cause: undefined }) still creates an own cause property, so the constructor must only pass options when a cause exists. The pre-existing error test used try/catch without rejects and would pass even if nothing threw.

What a human does next: on main of EyevinnOSC/client-ts run npm run lerna:version and push (expect a version above 0.28.0, probably 0.29.0, which also ships PRs 165 and 166). Test: cd "$(mktemp -d)" && npm pack @osaas/client-core@latest --silent >/dev/null && tar -xzOf osaas-client-core-*.tgz package/lib/fetch.js | grep -c cause prints a number above 0. Releasing/publishing is birme's.

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

Co-Authored-By: Claude <noreply@anthropic.com>
@birme birme self-assigned this Oct 24, 2026
@birme

birme commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

Research Review

Verified

  • Error cause option: root tsconfig.json has target: es2022 and lib: ["es2022"], which includes ErrorOptions/{ cause }. packages/core/tsconfig.json extends it, so super(message, { cause }) type-checks. CI ts check passes on head 8af3cbc.
  • cause === undefined ? undefined : { cause } guard: correct. new Error('x', { cause: undefined }) creates an own cause property, and the guard avoids that. The AC3 test asserts no own cause on HTTP errors.
  • HTTP error path: defaultErrorFactory constructs FetchError without cause, and error instanceof FetchError is rethrown unchanged. Status code, message and body shape are unchanged.
  • Non-enumerable cause claim: matches the ES2022 spec, where cause is installed non-enumerable. JSON.stringify and key enumeration are unaffected.
  • Old test: it used try/catch without rejects, so it passed when nothing threw. The rewrite is a genuine improvement.

Adjacent Stubs Detected

  • None found in fetch.ts, which has no TODO, no undefined : undefined stub and no empty catch.

Orphaned Files Detected

  • None. FetchError and createFetch are used across core, cli, web and transcode.

Escape Hatch Coverage

  • No new terminal-rejection branch. The change only adds data to an error that was already thrown.

Object Shape Verification

  • The one spread, { ...options }, is pre-existing and not changed by this PR.

Contract diff

  • createFetch and FetchError (packages/core/src/fetch.ts): status codes, message and httpCode unchanged. The only change is an added non-enumerable cause on non-HTTP failures. This is an additive optional field with no failure mode or timing change. No request handler or timeout changed.

Impact Analysis

  • Blast radius: all @osaas/client-core consumers that catch FetchError.
  • Downstream consumers: the PR body states a grep of cli, core, web and transcode. The search results match that set. A consumer outside these repos (for example osaas-app or osaas-ai) was not searched. This is additive, so it is acceptable.
  • Coordinated changes needed: a lerna version bump and publish, which the PR body assigns to birme.
  • Breaking change risk: low. Possible edge: a consumer that deep-equals or snapshots a FetchError (Jest toEqual also compares cause on Errors). I did not check the core.test.ts and cmd.test.ts assertions locally, but CI is green.

Risks Not Tested

  • Real undici fetch failed shape with cause.code was not exercised against a live network failure. The test uses a hand-built TypeError, which matches the documented undici shape.
  • Behavior in downstream repos after the package bump (the logging of cause is a separate concern in osaas-ai#2462).

Research Suggestions

  • None blocking. Optionally verify the npm pack command in the PR body after publish.

Verdict

No blocking issues found.


Reviewed by PR Research Validator. This review checks whether code is based on verified sources rather than assumptions.

@birme
birme merged commit ff39e6b into main Oct 24, 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.

2 participants