Skip to content

fix: error handling with Node VM#7280

Merged
Nick-Lucas merged 5 commits intotrpc:mainfrom
znikola:fix/7272
Mar 26, 2026
Merged

fix: error handling with Node VM#7280
Nick-Lucas merged 5 commits intotrpc:mainfrom
znikola:fix/7272

Conversation

@znikola
Copy link
Contributor

@znikola znikola commented Mar 22, 2026

Closes #7272

🎯 Changes

Fixed error handling when tRPC is used with Node VM by attempting to show the error message for implicitly thrown errors.

✅ Checklist

  • I have followed the steps listed in the Contributing guide.
  • If necessary, I have added documentation related to the changes made.
  • I have added or updated the tests related to the changes made.

Summary by CodeRabbit

  • Refactor
    • Improved runtime error conversion so created errors retain message text and original enumerable properties when origin is an unknown value.
  • Tests
    • Added regression tests covering non-string or missing messages, preservation of extra fields, and cross-realm VM error messages.

@znikola znikola requested a review from a team as a code owner March 22, 2026 18:58
@vercel
Copy link

vercel bot commented Mar 22, 2026

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

Project Deployment Actions Updated (UTC)
next-prisma-starter Ready Ready Preview Mar 24, 2026 6:17pm
og-image Ready Ready Preview, Comment Mar 24, 2026 6:17pm

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 22, 2026

Walkthrough

UnknownCauseError now takes a cause object in its constructor, sets the Error message from that cause, and assigns the cause's enumerable properties; getCauseFromUnknown now returns new UnknownCauseError(cause) instead of assigning properties after instantiation.

Changes

Cohort / File(s) Summary
Error construction
packages/server/src/unstable-core-do-not-import/error/TRPCError.ts
Changed UnknownCauseError to accept cause in constructor, derive message via getMessage(cause), and Object.assign(this, cause). getCauseFromUnknown now returns new UnknownCauseError(cause) for object causes.
Tests — VM/regression
packages/tests/server/regression/issue-7272-node-vm-error-message.test.ts
Added tests covering conversion of error-like objects (including cross-realm VM errors and non-string messages) to Error via getCauseFromUnknown/getTRPCErrorFromUnknown, ensuring message and properties are preserved.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • Nick-Lucas
  • KATT

Poem

🐰 With twitching nose and nimble paw,
I stitch the message where it once withdrew,
From cross-realm shadows I pull the law,
Now errors tell what they once knew. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title 'fix: error handling with Node VM' directly describes the main change: fixing error handling when tRPC is used with Node VM, which is the core objective of this PR.
Description check ✅ Passed PR description follows the template with issue reference (#7272), changes summary explaining the fix for Node VM error handling, and completed checklist items matching the implementation.
Linked Issues check ✅ Passed The PR fully addresses issue #7272 by implementing the proposed solution: modifying UnknownCauseError constructor to accept cause and call super(getMessage(cause)), updating getCauseFromUnknown to instantiate with new UnknownCauseError(cause), and adding comprehensive tests.
Out of Scope Changes check ✅ Passed All changes are directly in scope: UnknownCauseError modification, getMessage helper function, and regression tests for Node VM error handling—no unrelated alterations detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/server/src/unstable-core-do-not-import/error/TRPCError.ts`:
- Around line 7-10: The constructor currently uses Object.assign(this, cause)
which copies only enumerable properties and loses non-enumerable fields like
Error.prototype.stack; replace that assignment by copying all property
descriptors from the cause so non-enumerable properties are preserved (e.g., use
Object.getOwnPropertyDescriptors(cause) and Object.defineProperties(this, ...)
and guard for null/primitive causes) — update the constructor(cause: object) in
TRPCError.ts to use full descriptor copying instead of Object.assign(this,
cause) while still calling getMessage(cause) as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9682d153-f908-4b2b-89ea-bf8a1b46b9da

📥 Commits

Reviewing files that changed from the base of the PR and between e971f84 and 314d0b7.

📒 Files selected for processing (1)
  • packages/server/src/unstable-core-do-not-import/error/TRPCError.ts

Comment on lines +7 to +10
constructor(cause: object) {
super(getMessage(cause));
Object.assign(this, cause);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In JavaScript/Node.js, does Object.assign copy non-enumerable properties like Error.stack from an Error instance?

💡 Result:

No, Object.assign does not copy non-enumerable properties like Error.stack from an Error instance. Object.assign only copies enumerable own properties from source objects to the target. Error properties such as stack, message, and name are own properties but non-enumerable, so they are skipped. This behavior is consistent in both browser JavaScript and Node.js, as Object.assign follows the ECMAScript specification. To copy non-enumerable properties from an Error, use Object.getOwnPropertyDescriptors followed by Object.defineProperties, or manually assign specific properties like {stack: error.stack}.

Citations:


Stack trace lost when wrapping non-enumerable error properties

Line 9: Object.assign only copies enumerable properties. Error objects have non-enumerable properties like stack, so wrapping cross-realm or error-like objects causes stack traces to be lost.

Proposed patch
 class UnknownCauseError extends Error {
   [key: string]: unknown;

   constructor(cause: object) {
     super(getMessage(cause));
+    if ('stack' in cause && typeof (cause as { stack?: unknown }).stack === 'string') {
+      this.stack = (cause as { stack: string }).stack;
+    }
     Object.assign(this, cause);
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constructor(cause: object) {
super(getMessage(cause));
Object.assign(this, cause);
}
constructor(cause: object) {
super(getMessage(cause));
if ('stack' in cause && typeof (cause as { stack?: unknown }).stack === 'string') {
this.stack = (cause as { stack: string }).stack;
}
Object.assign(this, cause);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server/src/unstable-core-do-not-import/error/TRPCError.ts` around
lines 7 - 10, The constructor currently uses Object.assign(this, cause) which
copies only enumerable properties and loses non-enumerable fields like
Error.prototype.stack; replace that assignment by copying all property
descriptors from the cause so non-enumerable properties are preserved (e.g., use
Object.getOwnPropertyDescriptors(cause) and Object.defineProperties(this, ...)
and guard for null/primitive causes) — update the constructor(cause: object) in
TRPCError.ts to use full descriptor copying instead of Object.assign(this,
cause) while still calling getMessage(cause) as before.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 22, 2026

Open in StackBlitz

@trpc/client

npm i https://pkg.pr.new/@trpc/client@7280

@trpc/next

npm i https://pkg.pr.new/@trpc/next@7280

@trpc/openapi

npm i https://pkg.pr.new/@trpc/openapi@7280

@trpc/react-query

npm i https://pkg.pr.new/@trpc/react-query@7280

@trpc/server

npm i https://pkg.pr.new/@trpc/server@7280

@trpc/tanstack-react-query

npm i https://pkg.pr.new/@trpc/tanstack-react-query@7280

@trpc/upgrade

npm i https://pkg.pr.new/@trpc/upgrade@7280

commit: 671818a

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adjusts @trpc/server’s internal error-cause normalization to better preserve error messages when the thrown value comes from a different JS realm (e.g. Node’s vm), where instanceof Error can fail.

Changes:

  • Add a custom UnknownCauseError constructor that initializes the error message from the unknown “error-like” object.
  • Replace Object.assign(new UnknownCauseError(), cause) with new UnknownCauseError(cause) so message is captured even when non-enumerable.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}

function getMessage(cause: object) {
return (cause as Error).message;
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

getMessage(cause) reads (cause as Error).message and passes it directly to super(...). Because cause is unknown, accessing .message can throw (e.g. Proxy/getter), and non-string values like symbol can cause the Error constructor to throw during string coercion. Since this runs in the error-handling path, it should be best-effort and never throw—wrap the read/coercion in a try/catch and fall back to '' (or a safe default) if anything goes wrong.

Suggested change
return (cause as Error).message;
try {
const message = (cause as Error).message as unknown;
if (typeof message === 'string') {
return message;
}
if (
typeof message === 'number' ||
typeof message === 'boolean' ||
typeof message === 'bigint'
) {
return String(message);
}
if (typeof message === 'symbol') {
return message.description ?? '';
}
return '';
} catch {
return '';
}

Copilot uses AI. Check for mistakes.
@znikola
Copy link
Contributor Author

znikola commented Mar 23, 2026

UnknownCauseError now calls super with (cause as Error).message; VM-thrown plain objects often omit message, so diagnostics can go empty. nit: fall back to String(cause) when message is missing or not a string.

I updated the getMessage function to this:

function getMessage(cause: object) {
  if ('message' in cause) return String(cause.message);

  return undefined;
}

If I were to return String(cause) when there is no message property, the result would always be [object Object] because UnknownCauseError is always thrown in case the cause is an object:

  if (isObject(cause)) {
    return new UnknownCauseError(cause);
  }

Do you still think I should do it?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/tests/server/regression/issue-7272-node-vm-error-message.test.ts (1)

64-66: Use .toContain() for resilience against potential future error message format changes.

The test currently asserts the exact error message format with .toBe(). While the message format is consistent across Node 18.x and 20.x (the tested versions), using substring matching with .toContain() would make the test more resilient to potential V8/Node.js changes in future versions while still validating that the message is preserved.

Suggested assertion adjustment
-    expect(trpcError.message).toBe(
-      "Cannot read properties of null (reading 'property')",
-    );
+    expect(trpcError.message).toContain('Cannot read properties of null');
+    expect(trpcError.message).toContain("'property'");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tests/server/regression/issue-7272-node-vm-error-message.test.ts`
around lines 64 - 66, Replace the strict equality assertion on trpcError.message
with a substring check to make the test resilient to V8/Node message format
changes: locate the assertion using trpcError.message in the test (in
issue-7272-node-vm-error-message.test.ts) and change the
expect(...).toBe("Cannot read properties of null (reading 'property')") call to
use expect(...).toContain("Cannot read properties of null") so the test still
verifies the preserved message content without depending on exact formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/tests/server/regression/issue-7272-node-vm-error-message.test.ts`:
- Around line 64-66: Replace the strict equality assertion on trpcError.message
with a substring check to make the test resilient to V8/Node message format
changes: locate the assertion using trpcError.message in the test (in
issue-7272-node-vm-error-message.test.ts) and change the
expect(...).toBe("Cannot read properties of null (reading 'property')") call to
use expect(...).toContain("Cannot read properties of null") so the test still
verifies the preserved message content without depending on exact formatting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7f6461a9-6757-4285-b9b8-df63bba378c9

📥 Commits

Reviewing files that changed from the base of the PR and between 314d0b7 and 3d44d6e.

📒 Files selected for processing (2)
  • packages/server/src/unstable-core-do-not-import/error/TRPCError.ts
  • packages/tests/server/regression/issue-7272-node-vm-error-message.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/server/src/unstable-core-do-not-import/error/TRPCError.ts

@znikola
Copy link
Contributor Author

znikola commented Mar 26, 2026

Hi @Nick-Lucas , is there anything else I need to do in this PR?

@Nick-Lucas
Copy link
Contributor

Hey @znikola - busy week, sorry for the delay. Thanks for adding the test!

I can see it fails without the fix, and the fix seems straight-forward, so let's get this in

@Nick-Lucas Nick-Lucas merged commit a2f90fc into trpc:main Mar 26, 2026
48 checks 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.

bug: tRPC error handling with Node VM

3 participants