最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Using `instanceof` on objects created with constructors from deep npm dependencies - Stack Overflow

programmeradmin5浏览0评论

Background:

I have an npm module that I have common error handling code in, including a custom error:

function CustomError () { /* ... */ }
CustomError.prototype = Object.create(Error.prototype);
CustomError.prototype.constructor = CustomError;
module.exports = CustomError;

I have some other modules (let's call them 'module-a' and 'module-b') which both depend on the error handling module.

I also have some code with uses Bluebirds "filtered catch" functionality:

doSomething
.catch(CustomError, () => { /* ... */ });

The issue:

After some debugging I've discovered (somewhat obviously in hindsight) that errors created in 'module-a' are not instances of errors created by 'module-b'. This is because both modules have their own copy of the JS file containing the CustomError constructor, which are both run independently.

I would rather not have to resort to my current solution, which is basically:

CustomError.isCustomError = e => e.constructor.toString() === CustomError.toString();

and then:

doSomething
.then(CustomError.isCustomError, () => { /* ... */ });

This is clearly flimsy, and will fall apart if the versions fall out of sync.

So...

Is there some way to ensure that 'module-a' and 'module-b' both use the same instance of the constructor? Or another, less fragile solution.

Background:

I have an npm module that I have common error handling code in, including a custom error:

function CustomError () { /* ... */ }
CustomError.prototype = Object.create(Error.prototype);
CustomError.prototype.constructor = CustomError;
module.exports = CustomError;

I have some other modules (let's call them 'module-a' and 'module-b') which both depend on the error handling module.

I also have some code with uses Bluebirds "filtered catch" functionality:

doSomething
.catch(CustomError, () => { /* ... */ });

The issue:

After some debugging I've discovered (somewhat obviously in hindsight) that errors created in 'module-a' are not instances of errors created by 'module-b'. This is because both modules have their own copy of the JS file containing the CustomError constructor, which are both run independently.

I would rather not have to resort to my current solution, which is basically:

CustomError.isCustomError = e => e.constructor.toString() === CustomError.toString();

and then:

doSomething
.then(CustomError.isCustomError, () => { /* ... */ });

This is clearly flimsy, and will fall apart if the versions fall out of sync.

So...

Is there some way to ensure that 'module-a' and 'module-b' both use the same instance of the constructor? Or another, less fragile solution.

Share Improve this question asked Jan 11, 2017 at 10:05 phenomnomnominalphenomnomnominal 5,5151 gold badge31 silver badges48 bronze badges 1
  • The proper solution is a package manager that installs only one copy of the error handling library, so that both module-a and module-b will load the same CustomError – Bergi Commented Jul 8, 2024 at 19:38
Add a comment  | 

4 Answers 4

Reset to default 14

This is actually a problem in browsers too, when you have an iframe it gets its own copy of, for example, the Array constructor (making instanceof useless).

The solution for a custom constructor is to duck-type it. Here are some potential solutions with pros and cons.

  1. Check the constructor name. Pro: simple, well-supported. Con: better pick a fairly unique name to avoid false positives and forget about sub-classing it.

  2. Check the properties of the object (e.g. has both 'foo' and 'bar and 'foo' is a function). Pro: mostly fool-proof. Cons: fragile: this check may randomly break if you refactor your custom error class, relatively expensive.

  3. (Recommended) Add a property/method to act as a runtime type tag. This is how a number of libraries (for example, moment.js) handle this problem.

Code example (NOTE: updated for ES6):

class CustomError extends Error {
  static [Symbol.hasInstance](instance) {
    return instance && instance._isCustomError;
  }

  _isCustomError = true;
};

module.exports = {
  CustomError,
};

This is more or less exactly how moment detects whether or not a given object is a moment object.

Note about Symbols:

Please note that while Symbols might seem like a better choice here than _isCustomError but Symbols are globally unique, it doesn't solve the case where you have two instances of the constructor.

A simple typescript implementation if you don't expect conflicting class names:

type ClassConstructor<T> = new (...args: any[]) => T

export function isInstanceOf<T>(obj: any, cls: ClassConstructor<T>): boolean {
  if (!obj)
    return false;

  // Traverse the prototype chain to support inheritance
  let currentProto = Object.getPrototypeOf(obj);
  while (currentProto) {
    if (currentProto.constructor.name === cls.name)
      return true;
    
    currentProto = Object.getPrototypeOf(currentProto);
  }

  return false;
}

You can set some global variable to be the CustomError since the modules will still share the same global context.

function CustomError () { /* ... */ }
CustomError.prototype = Object.create(Error.prototype);
CustomError.prototype.constructor = CustomError;

const globalCustomError='CustomError'; //or whatever global name you want
if(!Object.getOwnPropertyDescriptor(global,globalCustomError))
  Object.defineProperty(global, globalCustomError, {value:CustomError});

module.exports = global[globalCustomError];

What do you mean by:

After some debugging I've discovered (somewhat obviously in hindsight) that errors created in 'module-a' are not instances of errors created by 'module-b'.

Error object can't be instance of another error object. Or you are saying that errors from module-a or module-b when doing something like err instanceof CustomError are returning different results? Having in mind that instanceof tests presence of constructor.prototype in object's prototype chain both errors from those modules should return true by code that you posted, when tested againt CustomError.

Can you show how you are creating those errors in those modules?

This is because both modules have their own copy of the JS file containing the CustomError constructor, which are both run independently.

Again, im confused by this statement. What do you mean by both modules have their own copy of something? Lets have small example:

// custom-error.js
'use strict'

function CustomError () {}

CustomError.prototype = Object.create(Error.prototype)
CustomError.prototype.constructor = CustomError

module.exports = CustomError

// module-a.js
const CustomError = require('./custom-error')
const err = new CustomError(...)
module.exports = err

// module-b.js
const CustomError = require('./custom-error')
const err = new CustomError(...)
module.exports = err

// dummy file to require those
const CustomError = require('./custom-error')
const errA = require('./module-a')
const errB = require('./module-b')

First both errA and errB should be an instanceof CustomError:

console.log(errA instanceof CustomError) // yields true
console.log(errB instanceof CustomError) // yields true

cunstructor property of errA and errB that will be found in prototype chain, should have reference that points to same function object CustomError

console.log(errA.constructor === errB.constructor) // yields true

Lets introduce also filtered catch example:

const Promise = require('bluebird')

Promise.resolve()
.then(() => {
  throw errA
})
.catch(CustomError, err => {
  console.log('instance of CustomError catched', err)
  throw errB
})
.catch(CustomError, err => {
  console.log('instance of CustomError catched again', err)
})

Results:

instance of CustomError catched [Error]
instance of CustomError catched again [Error]

And last thing, what do you mean in your example when you say deep npm dependencies? This CustomError thing is your module or 3rd party lib? The fact that is 3rd party module or not, that should not change anything.

发布评论

评论列表(0)

  1. 暂无评论