Visitar URL original
feat(errors): Recognize Feathers errors across duplicate module copies by fratzinger · Pull Request #3726 · feathersjs/feathers · GitHub
Skip to content

feat(errors): Recognize Feathers errors across duplicate module copies - #3726

Open
fratzinger wants to merge 1 commit into
v6from
feat/v6-error-guard
Open

fratzinger wants to merge 1 commit into
v6from
feat/v6-error-guard

Conversation

@fratzinger

Copy link
Copy Markdown
Member

Summary

instanceof FeathersError fails when an error comes from a different copy of feathers (multiple installed versions, mixed CJS/ESM, other realms). Errors are now branded with a global symbol, and the error classes check that brand in Symbol.hasInstance.

 const FEATHERS_ERROR = Symbol.for('feathers.error')

 class FeathersError extends Error {
+  static [Symbol.hasInstance]<T>(this: abstract new (...args: any[]) => T, value: unknown): value is T
+    prototype chain matches          → true
+    no string brand on value         → false
+    this === FeathersError           → true
+    this === built-in class for brand → true   // NotFound only matches 'NotFound'
   constructor(...)
     ...
+    Object.defineProperty(this, FEATHERS_ERROR, { value: name })   // non-enumerable
 }

Plus an explicit, typed guard:

isFeathersError(error)             // error is FeathersError
isFeathersError(error, 'NotFound') // error is NotFound
isFeathersError(error, 404)        // error is NotFound
isFeathersError(error, 418)        // type error
  • Symbol instead of a ~type string: payloads can't spoof it, and it is not copied by spread/Object.assign or emitted by toJSON.
  • instanceof still narrows to the specific class, including user subclasses. User subclasses only match by prototype chain, so new BadRequest() instanceof MyError stays false.
  • The HTTP error middleware now uses isFeathersError.
  • Adds the missing Gone entries to errors and the Errors interface. Before this, convert({ name: 'Gone' }) returned a plain Error.
  • Docs: new "Checking errors" section in api/errors.md.

Evidence

The new test loads a second module instance via vi.resetModules():

const duplicate = await import('../src/errors.js')
const error = new duplicate.NotFound()

error instanceof errors.FeathersError      // true
error instanceof errors.NotFound           // true
error instanceof errors.BadRequest         // false
errors.isFeathersError(error, 404)         // true
  • Before (errors.ts from v6): × recognizes errors from a duplicate module instance, AssertionError: The expression evaluated to a falsy value
    After: ✓ and the full feathers package suite passes (504 passed, 1 skipped)
  • Type narrowing (instanceof, isFeathersError by name and code, @ts-expect-error for unknown names and codes) was checked with tsc --noEmit.

Merge Danger

Door: two-way

The change adds to the public API (isFeathersError, errors.Gone). It is easy to revert as long as no one depends on it yet.

Blast Radius: low

instanceof checks against Feathers error classes can now return true for branded errors from another module copy. That is the intent. Behaviour for errors that were already recognized does not change. Errors from v5 @feathersjs/errors have no brand and are still not recognized.

🤖 Generated with Claude Code

`instanceof FeathersError` fails when multiple versions of feathers are
installed or CommonJS and ES modules are mixed. Feathers errors are now
branded with a global `Symbol.for('feathers.error')` and the error
classes implement `Symbol.hasInstance`, so `instanceof` works across
module copies while still narrowing to the specific error type.

Also adds a typed `isFeathersError(error, type?)` guard that accepts an
error name or status code, and adds the missing `Gone` entries to
`errors` and the `Errors` interface.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
readonly data: any
readonly errors: any

static [Symbol.hasInstance]<T>(this: abstract new (...args: any[]) => T, value: unknown): value is T {

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.

I'm not sure I understand what this is for. From what I know Symbol.for already ensures that you get the same Symbol instance across the entire runtime. I thought just defining the property and checking if it exists should work.

This branch has not been deployed

No deployments
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