Repository: nodejs/node
Pull request: assert: differentiate cases where `cause` is `undefined` or missing

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: lib/internal/assert/assertion_error.js
patch: @@ -9,6 +9,7 @@ const {
   ObjectAssign,
   ObjectDefineProperty,
   ObjectGetPrototypeOf,
+  ObjectPrototypeHasOwnProperty,
   String,
   StringPrototypeRepeat,
   StringPrototypeSlice,
@@ -49,8 +50,8 @@ function copyError(source) {
     __proto__: null,
     value: source.message,
   });
-  if (source.cause !== undefined) {
-    let cause = source.cause;
+  if (ObjectPrototypeHasOwnProperty(source, 'cause')) {
+    let { cause } = source;
 
     if (isError(cause)) {
       cause = copyError(cause);

filename: lib/internal/util/comparisons.js
patch: @@ -28,6 +28,7 @@ const {
 const { compare } = internalBinding('buffer');
 const assert = require('internal/assert');
 const { isURL } = require('internal/url');
+const { isError } = require('internal/util');
 const types = require('internal/util/types');
 const {
   isAnyArrayBuffer,
@@ -235,10 +236,10 @@ function innerDeepEqual(val1, val2, strict, memos) {
     if (!isAnyArrayBuffer(val2) || !areEqualArrayBuffers(val1, val2)) {
       return false;
     }
-  } else if (isNativeError(val1) || val1 instanceof Error) {
+  } else if (isError(val1)) {
     // Do not compare the stack as it might differ even though the error itself
     // is otherwise identical.
-    if (!isNativeError(val2) && !(val2 instanceof Error)) {
+    if (!isError(val2)) {
       return false;
     }
 
@@ -252,7 +253,9 @@ function innerDeepEqual(val1, val2, strict, memos) {
         (name1Enumerable !== ObjectPrototypePropertyIsEnumerable(val2, 'name') ||
           (!name1Enumerable && val1.name !== val2.name)) ||
         (cause1Enumerable !== ObjectPrototypePropertyIsEnumerable(val2, 'cause') ||
-          (!cause1Enumerable && !innerDeepEqual(val1.cause, val2.cause, strict, memos))) ||
+          (!cause1Enumerable && (
+            ObjectPrototypeHasOwnProperty(val1, 'cause') !== ObjectPrototypeHasOwnProperty(val2, 'cause') ||
+            !innerDeepEqual(val1.cause, val2.cause, strict, memos)))) ||
         (errors1Enumerable !== ObjectPrototypePropertyIsEnumerable(val2, 'errors') ||
          (!errors1Enumerable && !innerDeepEqual(val1.errors, val2.errors, strict, memos)))) {
       return false;

filename: test/parallel/test-assert-deep-with-error.js
patch: @@ -8,13 +8,26 @@ const defaultStartMessage = 'Expected values to be strictly deep-equal:\n' +
   '\n';
 
 test('Handle error causes', () => {
+  assert.deepStrictEqual(new Error('a', { cause: new Error('x') }), new Error('a', { cause: new Error('x') }));
+  assert.deepStrictEqual(
+    new Error('a', { cause: new RangeError('x') }),
+    new Error('a', { cause: new RangeError('x') }),
+  );
+
   assert.throws(() => {
     assert.deepStrictEqual(new Error('a', { cause: new Error('x') }), new Error('a', { cause: new Error('y') }));
   }, { message: defaultStartMessage + '  [Error: a] {\n' +
     '+   [cause]: [Error: x]\n' +
     '-   [cause]: [Error: y]\n' +
     '  }\n' });
 
+  assert.throws(() => {
+    assert.deepStrictEqual(new Error('a', { cause: new Error('x') }), new Error('a', { cause: new TypeError('x') }));
+  }, { message: defaultStartMessage + '  [Error: a] {\n' +
+    '+   [cause]: [Error: x]\n' +
+    '-   [cause]: [TypeError: x]\n' +
+    '  }\n' });
+
   assert.throws(() => {
     assert.deepStrictEqual(new Error('a'), new Error('a', { cause: new Error('y') }));
   }, { message: defaultStartMessage + '+ [Error: a]\n' +
@@ -37,3 +50,33 @@ test('Handle error causes', () => {
     new Error('a', { cause: { prop: 'a different value' } })
   );
 });
+
+test('Handle undefined causes', () => {
+  assert.deepStrictEqual(new Error('a', { cause: undefined }), new Error('a', { cause: undefined }));
+
+  assert.notDeepStrictEqual(new Error('a', { cause: 'undefined' }), new Error('a', { cause: undefined }));
+  assert.notDeepStrictEqual(new Error('a', { cause: undefined }), new Error('a'));
+  assert.notDeepStrictEqual(new Error('a'), new Error('a', { cause: undefined }));
+
+  assert.throws(() => {
+    assert.deepStrictEqual(new Error('a'), new Error('a', { cause: undefined }));
+  }, { message: defaultStartMessage +
+    '+ [Error: a]\n' +
+    '- [Error: a] {\n' +
+    '-   [cause]: undefined\n' +
+    '- }\n' });
+
+  assert.throws(() => {
+    assert.deepStrictEqual(new Error('a', { cause: undefined }), new Error('a'));
+  }, { message: defaultStartMessage +
+    '+ [Error: a] {\n' +
+    '+   [cause]: undefined\n' +
+    '+ }\n' +
+    '- [Error: a]\n' });
+  assert.throws(() => {
+    assert.deepStrictEqual(new Error('a', { cause: undefined }), new Error('a', { cause: 'undefined' }));
+  }, { message: defaultStartMessage + '  [Error: a] {\n' +
+    '+   [cause]: undefined\n' +
+    '-   [cause]: \'undefined\'\n' +
+    '  }\n' });
+});