diff --git a/lib/clone.js b/lib/clone.js index 7ecd0b3..dfbbcfd 100755 --- a/lib/clone.js +++ b/lib/clone.js @@ -6,7 +6,8 @@ const Utils = require('./utils'); const internals = { - needsProtoHack: new Set([Types.set, Types.map, Types.weakSet, Types.weakMap]) + needsProtoHack: new Set([Types.set, Types.map, Types.weakSet, Types.weakMap]), + structuredCloneExists: typeof structuredClone === 'function' }; @@ -86,6 +87,16 @@ module.exports = internals.clone = function (obj, options = {}, _seen = null) { continue; } + // Can only be covered in node 21+ + /* $lab:coverage:off$ */ + if (internals.structuredCloneExists && + baseProto === Types.error && + key === 'stack') { + + continue; // Already a part of the base object + } + /* $lab:coverage:on$ */ + const descriptor = Object.getOwnPropertyDescriptor(obj, key); if (descriptor) { if (descriptor.get || @@ -160,6 +171,17 @@ internals.base = function (obj, baseProto, options) { return newObj; } + // Can only be covered in node 21+ + /* $lab:coverage:off$ */ + else if (baseProto === Types.error && internals.structuredCloneExists) { + const err = structuredClone(obj); // Needed to copy internal stack state + if (proto !== baseProto) { + Object.setPrototypeOf(err, proto); // Fix prototype + } + + return err; + } + /* $lab:coverage:on$ */ if (internals.needsProtoHack.has(baseProto)) { const newObj = new proto.constructor(); diff --git a/test/clone.js b/test/clone.js index 7e73bd6..ae2b001 100755 --- a/test/clone.js +++ b/test/clone.js @@ -689,6 +689,59 @@ describe('clone()', () => { expect(b).to.not.shallow.equal(a); }); + it('clones Error', () => { + + class CustomError extends Error { + name = 'CustomError'; + } + + const a = new CustomError('bad'); + a.test = Symbol('test'); + + const b = Hoek.clone(a); + + expect(b).to.equal(a); + expect(b).to.not.shallow.equal(a); + expect(b).to.be.instanceOf(CustomError); + expect(b.stack).to.equal(a.stack); // Explicitly validate the .stack getters + }); + + it('clones Error with cause', { skip: process.version.startsWith('v14') }, () => { + + const a = new TypeError('bad', { cause: new Error('embedded') }); + const b = Hoek.clone(a); + + expect(b).to.equal(a); + expect(b).to.not.shallow.equal(a); + expect(b).to.be.instanceOf(TypeError); + expect(b.stack).to.equal(a.stack); // Explicitly validate the .stack getters + expect(b.cause.stack).to.equal(a.cause.stack); // Explicitly validate the .stack getters + }); + + it('clones Error with error message', () => { + + const a = new Error(); + a.message = new Error('message'); + + const b = Hoek.clone(a); + + //expect(b).to.equal(a); // deepEqual() always compares message using === + expect(b.message).to.equal(a.message); + expect(b.message).to.not.shallow.equal(a.message); + expect(b.stack).to.equal(a.stack); + }); + + it('cloned Error handles late stack update', () => { + + const a = new Error('bad'); + const b = Hoek.clone(a); + + a.stack = 'late update'; + + expect(b).to.equal(a); + expect(b.stack).to.not.equal(a.stack); + }); + it('ignores symbols', () => { const sym = Symbol();