JavaScript Basics: Copy Nested Data with structuredClone
Copying an object can still leave its nested data shared with the original. This tutorial compares a shallow copy with structuredClone(), then checks supported data types and a cloning failure.
All examples use built-in features and were tested on Linux with Node.js v24.19.0. Assertions stop the program if a result differs from the expected value. A successful run prints the output shown below. Node provides structuredClone() as a global, so no import is needed for it.
Prepare a scratch directory
You need Bash, a text editor, the mktemp, rm, and rmdir utilities, and Node.js on your PATH. Save source files as UTF-8; their .mjs extension selects ES modules. Run these commands in Bash and keep using the same terminal:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Create each named file with your editor. No package installation is needed.
1. Separate nested settings
Save this as copy-settings.mjs:
import assert from 'node:assert/strict';
const originalSettings = {
appearance: { theme: 'light' },
labels: ['draft'],
};
const shallowCopy = { ...originalSettings };
const independentCopy = structuredClone(originalSettings);
assert.notEqual(shallowCopy, originalSettings);
assert.equal(shallowCopy.appearance, originalSettings.appearance);
assert.notEqual(independentCopy.appearance, originalSettings.appearance);
assert.notEqual(independentCopy.labels, originalSettings.labels);
independentCopy.appearance.theme = 'dark';
independentCopy.labels.push('review');
assert.equal(originalSettings.appearance.theme, 'light');
assert.deepEqual(originalSettings.labels, ['draft']);
assert.deepEqual(independentCopy.labels, ['draft', 'review']);
console.log(`Original theme: ${originalSettings.appearance.theme}`);
console.log(`Copied theme: ${independentCopy.appearance.theme}`);
console.log(`Copied labels: ${independentCopy.labels.join(', ')}`);Run it:
node copy-settings.mjsOutput:
Original theme: light
Copied theme: dark
Copied labels: draft, reviewObject spread creates a new outer object, but here its nested objects remain shared. structuredClone() makes a deep copy of supported data. Updating the cloned theme or array therefore leaves the original untouched. Use a shallow copy when sharing nested values is intentional; a deep copy does more work.
2. Preserve useful types and relationships
Save this as clone-record.mjs:
import assert from 'node:assert/strict';
const originalRecord = {
createdAt: new Date('2026-10-17T00:00:00Z'),
counts: new Map([['ready', 2]]),
};
originalRecord.self = originalRecord;
const copiedRecord = structuredClone(originalRecord);
assert.notEqual(copiedRecord, originalRecord);
assert.equal(copiedRecord.self, copiedRecord);
assert.equal(copiedRecord.createdAt instanceof Date, true);
assert.notEqual(copiedRecord.createdAt, originalRecord.createdAt);
assert.equal(copiedRecord.createdAt.getTime(), originalRecord.createdAt.getTime());
assert.equal(copiedRecord.counts instanceof Map, true);
copiedRecord.counts.set('ready', 3);
assert.equal(originalRecord.counts.get('ready'), 2);
console.log(`Copied timestamp: ${copiedRecord.createdAt.toISOString()}`);
console.log(`Original count: ${originalRecord.counts.get('ready')}`);
console.log(`Copied count: ${copiedRecord.counts.get('ready')}`);
console.log(`Circular link preserved: ${copiedRecord.self === copiedRecord}`);Run it:
node clone-record.mjsOutput:
Copied timestamp: 2026-10-17T00:00:00.000Z
Original count: 2
Copied count: 3
Circular link preserved: trueThe clone has its own Date and Map. The circular self reference points to the copied record, not the original. This is one reason a JSON stringify/parse round trip is not an equivalent copying technique: it cannot handle this circular record and does not preserve these types in the same way.
3. Recognize data that cannot be cloned
Save this as clone-failure.mjs:
import assert from 'node:assert/strict';
const taskWithBehavior = {
title: 'Review draft',
run() { return 'done'; },
};
assert.throws(
() => structuredClone(taskWithBehavior),
{ name: 'DataCloneError' },
);
const taskData = { title: taskWithBehavior.title };
const copiedTaskData = structuredClone(taskData);
assert.deepEqual(copiedTaskData, { title: 'Review draft' });
assert.notEqual(copiedTaskData, taskData);
console.log('Function property: cloning rejected');
console.log(`Data-only title: ${copiedTaskData.title}`);Run it:
node clone-failure.mjsOutput:
Function property: cloning rejected
Data-only title: Review draftThe structured clone algorithm supports many data types, but not functions. Custom class prototypes and property descriptors are not generally preserved either, so treat this as copying supported data rather than duplicating arbitrary program behavior. Select data fields deliberately when an object also contains methods.
These examples omit the optional transfer list. Transferring a transferable object such as an ArrayBuffer moves its underlying resource and makes it unavailable on the original side; that is a different operation from the independent copies demonstrated here.
Clean up
In the same terminal, remove only the files created here, return to your original directory, and remove the now-empty scratch directory:
rm -- copy-settings.mjs clone-record.mjs clone-failure.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.