Skip to content

bug: tRPC error handling with Node VM #7272

@znikola

Description

@znikola

Provide environment information

System:
  OS: macOS 26.3.1
  CPU: (11) arm64 Apple M3 Pro
  Memory: 2.63 GB / 36.00 GB
Binaries:
  Node: 24.14.0 - /Users/<user>/.nvm/versions/node/v24.14.0/bin/node
  npm: 11.9.0 - /Users/<user>/.nvm/versions/node/v24.14.0/bin/npm
  pnpm: 10.32.1 - /opt/homebrew/bin/pnpm
Browsers:
  Chrome: 146.0.7680.153
  Firefox Developer Edition: 149.0
  Safari: 26.3.1
npmPackages:
  @trpc/server: ^11.13.4 => 11.13.4
  typescript: ^5.9.3 => 5.9.3

Describe the bug

Problem

While using Node's VM, errors that are implicitly thrown by the JavaScript engine (e.g. TypeError) use VM's own internal built-in constructors, meaning instanceof checks for Error will fail because the constructors are different:

// packages/server/src/unstable-core-do-not-import/error/TRPCError.ts

export function getCauseFromUnknown(cause: unknown): Error | undefined {
  if (cause instanceof Error) { // 👈 returns false
    return cause;
  }
  ...
}

Because of this, tRPC ends up throwing an error without a message:

{
  "error": {
    "message": "",
    "code": -32603,
    "data": {
      "code": "INTERNAL_SERVER_ERROR",
      "httpStatus": 500,
      "stack": "TRPCError: \n    at getTRPCErrorFromUnknown ...",
      "path": "greet"
    }
  }
}

Link to reproduction

https://github.com/znikola/trpc-error-handling-reproduction

To reproduce

Since Error's message and stack properties are non-enumerable, they won't be copied by Object.assign().

// packages/server/src/unstable-core-do-not-import/error/TRPCError.ts

export function getCauseFromUnknown(cause: unknown): Error | undefined {
  ...
  // If it's an object, we'll create a synthetic error
  if (isObject(cause)) {
    return Object.assign(new UnknownCauseError(), cause); // 👈 message and stack won't be copied
}

This is the proposed change to the UnknownCauseError:

// packages/server/src/unstable-core-do-not-import/error/TRPCError.ts

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

  constructor(cause: object) {
    super(getMessage(cause));
    Object.assign(this, cause);
  }
}

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

and throw it like this:

// packages/server/src/unstable-core-do-not-import/error/TRPCError.ts
export function getCauseFromUnknown(cause: unknown): Error | undefined {
  ...

  // If it's an object, we'll create a synthetic error
  if (isObject(cause)) {
    return new UnknownCauseError(cause);
  }

  return undefined;
}

This was verified by patching the getCauseFromUnknown() function in the latest version of @trpc/server (11.13.4) in the node_modules:

// node_modules/@trpc/server/dist/tracked-Bjtgv3wJ.mjs

...

var UnknownCauseError = class extends Error {
  constructor(cause) {
    super(getMessage(cause));
    Object.assign(this, cause);
  }
};
function getMessage(cause) {
  return (cause).message;
}
function getCauseFromUnknown(cause) {
	...
  // 👇 old
	// if (isObject(cause)) return Object.assign(new UnknownCauseError(), cause);
  // 👇 new
  if (isObject(cause)) return new UnknownCauseError(cause);
	return void 0;
}
...

Additional information

No response

👨‍👧‍👦 Contributing

  • 🙋‍♂️ Yes, I'd be down to file a PR fixing this bug!

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions