Skip to content

Commit 8a4266e

Browse files
committed
fix: missing stack starting from node 21+
1 parent 01e8086 commit 8a4266e

File tree

2 files changed

+76
-1
lines changed

2 files changed

+76
-1
lines changed

lib/clone.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ const Utils = require('./utils');
66

77

88
const internals = {
9-
needsProtoHack: new Set([Types.set, Types.map, Types.weakSet, Types.weakMap])
9+
needsProtoHack: new Set([Types.set, Types.map, Types.weakSet, Types.weakMap]),
10+
structuredCloneExists: typeof structuredClone === 'function'
1011
};
1112

1213

@@ -86,6 +87,16 @@ module.exports = internals.clone = function (obj, options = {}, _seen = null) {
8687
continue;
8788
}
8889

90+
// Can only be covered in node 21+
91+
/* $lab:coverage:off$ */
92+
if (internals.structuredCloneExists &&
93+
baseProto === Types.error &&
94+
key === 'stack') {
95+
96+
continue; // Already a part of the base object
97+
}
98+
/* $lab:coverage:on$ */
99+
89100
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
90101
if (descriptor) {
91102
if (descriptor.get ||
@@ -160,6 +171,17 @@ internals.base = function (obj, baseProto, options) {
160171

161172
return newObj;
162173
}
174+
// Can only be covered in node 21+
175+
/* $lab:coverage:off$ */
176+
else if (baseProto === Types.error && internals.structuredCloneExists) {
177+
const err = structuredClone(obj); // Needed to copy internal stack state
178+
if (proto !== baseProto) {
179+
Object.setPrototypeOf(err, proto); // Fix prototype
180+
}
181+
182+
return err;
183+
}
184+
/* $lab:coverage:on$ */
163185

164186
if (internals.needsProtoHack.has(baseProto)) {
165187
const newObj = new proto.constructor();

test/clone.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,59 @@ describe('clone()', () => {
689689
expect(b).to.not.shallow.equal(a);
690690
});
691691

692+
it('clones Error', () => {
693+
694+
class CustomError extends Error {
695+
name = 'CustomError';
696+
}
697+
698+
const a = new CustomError('bad');
699+
a.test = Symbol('test');
700+
701+
const b = Hoek.clone(a);
702+
703+
expect(b).to.equal(a);
704+
expect(b).to.not.shallow.equal(a);
705+
expect(b).to.be.instanceOf(CustomError);
706+
expect(b.stack).to.equal(a.stack); // Explicitly validate the .stack getters
707+
});
708+
709+
it('clones Error with cause', { skip: process.version.startsWith('v14') }, () => {
710+
711+
const a = new TypeError('bad', { cause: new Error('embedded') });
712+
const b = Hoek.clone(a);
713+
714+
expect(b).to.equal(a);
715+
expect(b).to.not.shallow.equal(a);
716+
expect(b).to.be.instanceOf(TypeError);
717+
expect(b.stack).to.equal(a.stack); // Explicitly validate the .stack getters
718+
expect(b.cause.stack).to.equal(a.cause.stack); // Explicitly validate the .stack getters
719+
});
720+
721+
it('clones Error with error message', () => {
722+
723+
const a = new Error();
724+
a.message = new Error('message');
725+
726+
const b = Hoek.clone(a);
727+
728+
//expect(b).to.equal(a); // deepEqual() always compares message using ===
729+
expect(b.message).to.equal(a.message);
730+
expect(b.message).to.not.shallow.equal(a.message);
731+
expect(b.stack).to.equal(a.stack);
732+
});
733+
734+
it('cloned Error handles late stack update', () => {
735+
736+
const a = new Error('bad');
737+
const b = Hoek.clone(a);
738+
739+
a.stack = 'late update';
740+
741+
expect(b).to.equal(a);
742+
expect(b.stack).to.not.equal(a.stack);
743+
});
744+
692745
it('ignores symbols', () => {
693746

694747
const sym = Symbol();

0 commit comments

Comments
 (0)