Skip to content

fix(client): preserve subclass prototype chain in cloneORPCError - #1799

Closed
Wadiou wants to merge 1 commit into
middleapi:mainfrom
Wadiou:fix/clone-orpc-error-subclass-prototype
Closed

fix(client): preserve subclass prototype chain in cloneORPCError#1799
Wadiou wants to merge 1 commit into
middleapi:mainfrom
Wadiou:fix/clone-orpc-error-subclass-prototype

Conversation

@Wadiou

@Wadiou Wadiou commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

fix(client): preserve subclass prototype chain in cloneORPCError

Problem

cloneORPCError instantiates new ORPCError(error.code, ...) directly, stripping custom subclass prototypes (e.g. class CustomError extends ORPCError). This causes error instanceof CustomError checks to fail downstream during procedure error reconciliation.

Solution

  • Use Object.create(Object.getPrototypeOf(error)) to preserve the subclass prototype chain.
  • Explicitly copy .message, .stack, and .cause to preserve non-enumerable error properties.
  • Update cloneORPCError signature to <T extends AnyORPCError>(error: T): T.

Tests

  • Added unit test in packages/client/src/error-utils.test.ts verifying subclass prototype preservation and custom property copying. All 23 tests pass.

`cloneORPCError` previously hardcoded `new ORPCError(error.code, ...)`, which stripped custom subclass prototypes from cloned error instances. During error reconciliation, this caused `error instanceof CustomSubclass` checks to fail downstream.

Update `cloneORPCError` to use `Object.create(Object.getPrototypeOf(error))` to preserve the prototype chain, while explicitly copying `.message`, `.stack`, and `.cause` to ensure non-enumerable Error properties and cause chains are retained cleanly.
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
orpc Ready Ready Preview Aug 7, 2026 12:30am

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • cloneORPCError clones via Object.create(Object.getPrototypeOf(error)) + Object.assign – preserves the subclass prototype chain instead of always producing a base ORPCError, with message/stack/cause (non-enumerable on Error instances) copied explicitly.
  • Signature generalized to <T extends AnyORPCError>(error: T): T – the returned type now reflects the actual (possibly subclassed) error.
  • New unit test asserting instanceof CustomSubclassError, instanceof ORPCError, and custom field/message preservation.

The change is sound: code, data, name, and subclass fields are own enumerable properties so Object.assign captures them; the explicitly-copied message/stack/cause cover the non-enumerable Error props. Since the clone keeps the original object's prototype, all three instanceof paths hold — including the cross-context WeakSet walk via getConstructors — and because the prototype is inherited from the source instance itself, it's actually more robust than the prior new ORPCError(...) approach. All three callers (packages/contract/src/error-utils.ts, packages/server/src/procedure-client.ts:277, packages/json-schema/src/smart-coercion-link-plugin.ts:77) only mutate data/defined/inferable after cloning, which this implementation preserves. The new test genuinely fails against the old code, so it pins the regression. Verified locally: all 23 client + 19 contract error-utils tests pass.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pkg-pr-new

pkg-pr-new Bot commented Aug 7, 2026

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

npm i https://pkg.pr.new/@orpc/ai-sdk@1799

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@1799

@orpc/bun

npm i https://pkg.pr.new/@orpc/bun@1799

@orpc/client

npm i https://pkg.pr.new/@orpc/client@1799

@orpc/cloudflare

npm i https://pkg.pr.new/@orpc/cloudflare@1799

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@1799

@orpc/experimental-effect

npm i https://pkg.pr.new/@orpc/experimental-effect@1799

@orpc/evlog

npm i https://pkg.pr.new/@orpc/evlog@1799

@orpc/hibernation

npm i https://pkg.pr.new/@orpc/hibernation@1799

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@1799

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@1799

@orpc/next

npm i https://pkg.pr.new/@orpc/next@1799

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@1799

@orpc/opentelemetry

npm i https://pkg.pr.new/@orpc/opentelemetry@1799

@orpc/pinia-colada

npm i https://pkg.pr.new/@orpc/pinia-colada@1799

@orpc/pino

npm i https://pkg.pr.new/@orpc/pino@1799

@orpc/publisher

npm i https://pkg.pr.new/@orpc/publisher@1799

@orpc/ratelimit

npm i https://pkg.pr.new/@orpc/ratelimit@1799

@orpc/server

npm i https://pkg.pr.new/@orpc/server@1799

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@1799

@orpc/swr

npm i https://pkg.pr.new/@orpc/swr@1799

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@1799

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@1799

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@1799

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@1799

commit: dcd4183

@codspeed-hq

codspeed-hq Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 25 untouched benchmarks


Comparing Wadiou:fix/clone-orpc-error-subclass-prototype (dcd4183) with main (324bd7a)

Open in CodSpeed

@dinwwwh dinwwwh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix! The goal is legitimate and the approach is close. I verified locally: all 23 tests pass, repo-wide tsc is clean, and instanceof Error / instanceof ORPCError / instanceof CustomSubclassError all hold.

There is one regression to address before merging.

The clone is no longer a real Error

Object.create skips the Error constructor, so the clone lacks the [[ErrorData]] internal slot, and the manually assigned message / stack / cause become own enumerable properties (on real errors they are non-enumerable). Confirmed with the PR applied:

  • util.types.isNativeError(cloned)false, Object.prototype.toString.call(cloned)[object Object]
  • Object.keys(original)[name, defined, inferable, code, data], but Object.keys(cloned) additionally contains message, stack, cause

Practical impact:

  • Cloning happens automatically on the server path (reconcileORPCError, procedure-client.ts), so every reconciled error users catch has this shape. User code doing { ...error } or iterating keys now picks up the stack trace and cause, which it did not before. oRPC's own wire serialization goes through toJSON() and is unaffected.
  • structuredClone / postMessage of a cloned error no longer round-trips as an Error, which matters for message-port/worker transports.

Suggested fix

Construct a real error first, then swap the prototype. This keeps the subclass prototype chain, native-error semantics, and copies every own property (including custom subclass fields and non-enumerable stack) with its exact descriptor:

export function cloneORPCError<T extends AnyORPCError>(error: T): T {
  const cloned = new ORPCError(error.code, {
    message: error.message,
    data: error.data,
    cause: error.cause,
  })

  Object.setPrototypeOf(cloned, Object.getPrototypeOf(error))
  Object.defineProperties(cloned, Object.getOwnPropertyDescriptors(error))

  return cloned as T
}

This also makes the explicit message / stack / cause / defined / inferable assignments unnecessary.

Minor

  • Subclass private fields (#field) still throw on the clone since the subclass constructor never runs. This is inherent to any constructor-skipping clone (the suggestion above included); a short JSDoc note would help since (error: T): T promises full fidelity.
  • The explicit defined / inferable reassignments are redundant with Object.assign in the current version.
  • Test suggestion: assert Object.keys(cloned) matches Object.keys(original), and that stack and cause are preserved. That would have caught the enumerability issue.

@dinwwwh

dinwwwh commented Aug 7, 2026

Copy link
Copy Markdown
Member

Thanks again @Wadiou! Closing in favor of #1801, which fixes the same issue while keeping the clone a native Error (non-enumerable message/stack/cause, structuredClone round-trips). You are credited as co-author there.

@dinwwwh dinwwwh closed this Aug 7, 2026
dinwwwh added a commit that referenced this pull request Aug 7, 2026
`cloneORPCError` rebuilt errors with `new ORPCError(...)`, which
stripped custom subclass prototypes, so `error instanceof CustomError`
failed after error reconciliation. The clone is now constructed as a
real `ORPCError` whose prototype is swapped to the original's, so
subclass instances stay `instanceof` their class while the clone remains
a native `Error`.

Supersedes #1799, thanks @Wadiou for the report and the initial
approach.

## Fixes

- Cloned errors are `instanceof` their subclass, and custom properties
are copied with their exact descriptors.
- Unlike the `Object.create` approach in #1799, the clone keeps native
`Error` semantics: `message` / `stack` / `cause` stay non-enumerable, so
`{ ...error }` and key iteration do not expose stack traces, and
`structuredClone` / `postMessage` still round-trip.
- Signature widened to `<T extends AnyORPCError>(error: T): T`, backward
compatible.

## Testing

- New tests cover subclass prototype preservation and clone shape /
native-error semantics; the shape test fails under the #1799 approach.
- All client, contract, server, and json-schema tests pass (1157), `tsc`
clean.

---------

Co-authored-by: Wadoud <wadiouyt@gmail.com>
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