Skip to content

Commit 62e2d51

Browse files
meeberkeithamus
authored andcommitted
feat: change error comparison algorithm (#57)
* chore: drop support for Node versions older than 6 BREAKING CHANGE: This commit drops support for versions of Node that are no longer maintained. * feat: change error comparison algorithm BREAKING CHANGE: Previously, `Error` objects were compared using strict equality. This commit causes `Error` objects to be compared using deep equality instead, but treats them as a special case by including their `name` and `message` properties in the comparison, regardless of enumerability.
1 parent 04d6da6 commit 62e2d51

5 files changed

Lines changed: 81 additions & 15 deletions

File tree

.travis.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ cache:
66
directories:
77
- node_modules
88
node_js:
9-
- 4 # to be removed 2018-04-01
10-
- 6 # to be removed 2019-04-01
9+
- 6 # to be removed 2019-04-30
10+
- 8 # to be removed 2019-12-31
11+
- 10 # to be removed 2021-04-30
1112
- lts/* # safety net; don't remove
1213
- node # safety net; don't remove
1314
before_install:

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,11 @@ The primary export of `deep-eql` is function that can be given two objects to co
108108
- All own and inherited enumerable properties are considered:
109109
- `eql(Object.create({ foo: { a: 1 } }), Object.create({ foo: { a: 1 } })).should.be.true;`
110110
- `eql(Object.create({ foo: { a: 1 } }), Object.create({ foo: { a: 2 } })).should.be.false;`
111+
- When comparing `Error` objects, `name` and `message` properties are also considered, regardless of enumerability:
112+
- `eql(Error('foo'), Error('foo')).should.be.true;`
113+
- `eql(Error('foo'), Error('bar')).should.be.false;`
114+
- `eql(Error('foo'), TypeError('foo')).should.be.false;`
115+
- `eql(Object.assign(Error('foo'), { code: 42 }), Object.assign(Error('foo'), { code: 13 })).should.be.false;`
111116
- Arguments are not Arrays:
112117
- `eql([], arguments).should.be.false;`
113118
- `eql([], Array.prototype.slice.call(arguments)).should.be.true;`
114-
- Error objects are compared by reference (see https://github.com/chaijs/chai/issues/608):
115-
- `eql(new Error('msg'), new Error('msg')).should.be.false;`
116-
- `var err = new Error('msg'); eql(err, err).should.be.true;`

index.js

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,6 @@ function extensiveDeepEqualByType(leftHandOperand, rightHandOperand, leftHandTyp
207207
case 'function':
208208
case 'WeakMap':
209209
case 'WeakSet':
210-
case 'Error':
211210
return leftHandOperand === rightHandOperand;
212211
case 'Arguments':
213212
case 'Int8Array':
@@ -234,7 +233,7 @@ function extensiveDeepEqualByType(leftHandOperand, rightHandOperand, leftHandTyp
234233
case 'Map':
235234
return entriesEqual(leftHandOperand, rightHandOperand, options);
236235
default:
237-
return objectEqual(leftHandOperand, rightHandOperand, options);
236+
return objectEqual(leftHandOperand, rightHandOperand, leftHandType, options);
238237
}
239238
}
240239

@@ -378,6 +377,21 @@ function getEnumerableKeys(target) {
378377
return keys;
379378
}
380379

380+
/*!
381+
* Adds `Error`-specific keys to an array of object keys. We want to include these keys when comparing `Error` objects
382+
* despite these keys being non-enumerable by default (and thus not added by `getEnumerableKeys`).
383+
*
384+
* @param {Array} keys An array of keys
385+
*/
386+
function addExtraErrorKeys(keys) {
387+
if (keys.indexOf('name') === -1) { // eslint-disable-line no-magic-numbers
388+
keys.push('name');
389+
}
390+
if (keys.indexOf('message') === -1) { // eslint-disable-line no-magic-numbers
391+
keys.push('message');
392+
}
393+
}
394+
381395
/*!
382396
* Determines if two objects have matching values, given a set of keys. Defers to deepEqual for the equality check of
383397
* each key. If any value of the given key is not equal, the function will return false (early).
@@ -403,17 +417,22 @@ function keysEqual(leftHandOperand, rightHandOperand, keys, options) {
403417

404418
/*!
405419
* Recursively check the equality of two Objects. Once basic sameness has been established it will defer to `deepEqual`
406-
* for each enumerable key in the object.
420+
* for each enumerable key in the object. For `Error` objects, also compare `name` and `message` keys, regardless of
421+
* enumerability.
407422
*
408423
* @param {Mixed} leftHandOperand
409424
* @param {Mixed} rightHandOperand
425+
* @param {String} type of leftHandOperand
410426
* @param {Object} [options] (Optional)
411427
* @return {Boolean} result
412428
*/
413-
414-
function objectEqual(leftHandOperand, rightHandOperand, options) {
429+
function objectEqual(leftHandOperand, rightHandOperand, leftHandType, options) {
415430
var leftHandKeys = getEnumerableKeys(leftHandOperand);
416431
var rightHandKeys = getEnumerableKeys(rightHandOperand);
432+
if (leftHandType === 'Error') {
433+
addExtraErrorKeys(leftHandKeys);
434+
addExtraErrorKeys(rightHandKeys);
435+
}
417436
if (leftHandKeys.length && leftHandKeys.length === rightHandKeys.length) {
418437
leftHandKeys.sort();
419438
rightHandKeys.sort();

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,6 @@
8484
"watchify": "^3.7.0"
8585
},
8686
"engines": {
87-
"node": ">=0.12"
87+
"node": ">=6"
8888
}
8989
}

test/index.js

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -379,13 +379,57 @@ describe('Generic', function () {
379379
describe('errors', function () {
380380

381381
it('returns true for same errors', function () {
382-
var error = new Error('foo');
382+
var error = Error('foo');
383383
assert(eql(error, error), 'eql(error, error)');
384384
});
385385

386-
it('returns false for different errors', function () {
387-
assert(eql(new Error('foo'), new Error('foo')) === false,
388-
'eql(new Error("foo"), new Error("foo")) === false');
386+
it('returns true for errors with same name and message', function () {
387+
assert(eql(Error('foo'), Error('foo')),
388+
'eql(Error("foo"), Error("foo"))');
389+
});
390+
391+
it('returns true for errors with same name and message despite different constructors', function () {
392+
var err1 = Error('foo');
393+
var err2 = TypeError('foo');
394+
err2.name = 'Error';
395+
assert(eql(err1, err2),
396+
'eql(Error("foo"), Object.assign(TypeError("foo"), { name: "Error" }))');
397+
});
398+
399+
it('returns false for errors with same name but different messages', function () {
400+
assert(eql(Error('foo'), Error('bar')) === false,
401+
'eql(Error("foo"), Error("bar")) === false');
402+
});
403+
404+
it('returns false for errors with same message but different names', function () {
405+
assert(eql(Error('foo'), TypeError('foo')) === false,
406+
'eql(Error("foo"), TypeError("foo")) === false');
407+
});
408+
409+
it('returns false for errors with same message but different names despite same constructors', function () {
410+
var err1 = Error('foo');
411+
var err2 = Error('foo');
412+
err2.name = 'TypeError';
413+
assert(eql(err1, err2) === false,
414+
'eql(Error("foo"), Object.assign(Error("foo"), { name: "TypeError" })) === false');
415+
});
416+
417+
it('returns true for errors with same custom property', function () {
418+
var err1 = Error('foo');
419+
var err2 = Error('foo');
420+
err1.code = 42;
421+
err2.code = 42;
422+
assert(eql(err1, err2),
423+
'eql(Object.assign(Error("foo"), { code: 42 }), Object.assign(Error("foo"), { code: 42 }))');
424+
});
425+
426+
it('returns false for errors with different custom property', function () {
427+
var err1 = Error('foo');
428+
var err2 = Error('foo');
429+
err1.code = 42;
430+
err2.code = 13;
431+
assert(eql(err1, err2) === false,
432+
'eql(Object.assign(new Error("foo"), { code: 42 }), Object.assign(new Error("foo"), { code: 13 })) === false');
389433
});
390434

391435
});

0 commit comments

Comments
 (0)