Skip to main content

How to Work with JSON in JavaScript

JSON (JavaScript Object Notation) is the universal data exchange format of the web. Every time your application communicates with an API, stores configuration, saves user preferences, or sends data between a browser and a server, JSON is almost certainly the format carrying that data. Despite its name containing "JavaScript," JSON is language-independent and supported by virtually every programming language and platform.

JSON's strength lies in its simplicity: it is a text-based format that is both human-readable and machine-parsable. JavaScript provides two built-in methods, JSON.stringify() and JSON.parse(), that handle the conversion between JavaScript values and JSON strings. While these methods are straightforward for basic use, they offer powerful customization options (replacers, revivers, custom serialization) and have important limitations that every developer should understand.

This guide covers JSON's syntax rules, both conversion methods with their advanced features, the types JSON cannot represent, custom serialization hooks, and the practical question of using JSON for deep cloning versus modern alternatives.

What Is JSON? (Syntax Rules, Valid Types)

JSON is a text format for representing structured data. It was derived from JavaScript object literal syntax but has stricter rules.

A JSON Example

{
"name": "Alice",
"age": 30,
"isActive": true,
"address": {
"city": "Rome",
"country": "Italy"
},
"hobbies": ["reading", "coding", "hiking"],
"spouse": null
}

JSON Syntax Rules

JSON is more restrictive than JavaScript object literals. Breaking any rule makes the JSON invalid.

1. Property names must be double-quoted strings:

// ✅ Valid JSON
{ "name": "Alice" }

// ❌ Invalid: single quotes
{ 'name': 'Alice' }

// ❌ Invalid: unquoted key
{ name: "Alice" }

2. String values must use double quotes:

// ✅ Valid
{ "greeting": "hello" }

// ❌ Invalid: single quotes
{ "greeting": 'hello' }

// ❌ Invalid: backticks
{ "greeting": `hello` }

3. No trailing commas:

// ✅ Valid
{ "a": 1, "b": 2 }

// ❌ Invalid: trailing comma
{ "a": 1, "b": 2, }

// ❌ Invalid: trailing comma in array
[1, 2, 3,]

4. No comments:

// ❌ Invalid: no comments allowed in JSON
{
// This is not allowed
"name": "Alice" /* neither is this */
}

5. No undefined, functions, or special values:

JSON supports only a specific set of value types.

Valid JSON Types

TypeJSON RepresentationExample
StringDouble-quoted"hello"
NumberInteger or float42, 3.14, -7, 1e10
Booleantrue or falsetrue
Nullnullnull
Object{} with key-value pairs{"a": 1}
Array[] with values[1, 2, 3]

Not valid in JSON: undefined, NaN, Infinity, -Infinity, functions, symbols, Date objects, Map, Set, RegExp, or any other JavaScript-specific type.

JSON as a Standalone Value

A valid JSON document does not have to be an object. It can be any JSON value:

// All of these are valid JSON documents:
console.log(JSON.parse('"hello"')); // "hello" (string)
console.log(JSON.parse('42')); // 42 (number)
console.log(JSON.parse('true')); // true (boolean)
console.log(JSON.parse('null')); // null
console.log(JSON.parse('[1,2,3]')); // [1, 2, 3] (array)
console.log(JSON.parse('{"a":1}')); // { a: 1 } (object)

JSON.stringify(): Object to JSON String

JSON.stringify() converts a JavaScript value into a JSON-formatted string.

Basic Usage

let user = {
name: "Alice",
age: 30,
isActive: true,
hobbies: ["reading", "coding"]
};

let json = JSON.stringify(user);
console.log(json);
// '{"name":"Alice","age":30,"isActive":true,"hobbies":["reading","coding"]}'

console.log(typeof json);
// "string"

What Gets Stringified

let data = {
string: "hello",
number: 42,
float: 3.14,
boolean: true,
nullVal: null,
array: [1, 2, 3],
nested: { a: 1, b: 2 }
};

console.log(JSON.stringify(data));
// '{"string":"hello","number":42,"float":3.14,"boolean":true,"nullVal":null,"array":[1,2,3],"nested":{"a":1,"b":2}}'

What Gets Silently Skipped

Properties with certain value types are silently omitted from the output:

let data = {
name: "Alice",
greet: function() { return "hi"; }, // Function (SKIPPED)
id: Symbol("id"), // Symbol (SKIPPED)
nothing: undefined, // undefined (SKIPPED)
age: 30
};

console.log(JSON.stringify(data));
// '{"name":"Alice","age":30}'
// greet, id, and nothing are gone!

Arrays Treat Skipped Values Differently

In arrays, unsupported values are replaced with null instead of being removed (to preserve indices):

let arr = [1, function() {}, undefined, Symbol("x"), "hello"];

console.log(JSON.stringify(arr));
// '[1,null,null,null,"hello"]'
// Functions, undefined, and symbols become null in arrays

Special Number Values

console.log(JSON.stringify(NaN));       // "null"
console.log(JSON.stringify(Infinity)); // "null"
console.log(JSON.stringify(-Infinity)); // "null"

let data = { a: NaN, b: Infinity, c: -Infinity };
console.log(JSON.stringify(data));
// '{"a":null,"b":null,"c":null}'

Stringifying Primitives

JSON.stringify works with standalone values too:

console.log(JSON.stringify("hello"));   // '"hello"' (with quotes)
console.log(JSON.stringify(42)); // '42'
console.log(JSON.stringify(true)); // 'true'
console.log(JSON.stringify(null)); // 'null'
console.log(JSON.stringify(undefined)); // undefined (not a string!)

Note that JSON.stringify(undefined) returns the JavaScript value undefined, not the string "undefined". This is a special case.

Date Objects

Dates are automatically converted to their ISO string representation:

let event = {
title: "Meeting",
date: new Date("2024-01-15T14:30:00Z")
};

console.log(JSON.stringify(event));
// '{"title":"Meeting","date":"2024-01-15T14:30:00.000Z"}'

// The date becomes a STRING in JSON, not a Date object
// When parsed back, it will be a string, not a Date!

Replacer Function and Array: Controlling Serialization

JSON.stringify() accepts a second argument called the replacer that controls which properties are included and how values are transformed.

Replacer as an Array

Pass an array of property names to include only those properties:

let user = {
name: "Alice",
age: 30,
password: "secret123",
email: "alice@example.com",
role: "admin"
};

// Only include name and email
let json = JSON.stringify(user, ["name", "email"]);
console.log(json);
// '{"name":"Alice","email":"alice@example.com"}'

This is useful for filtering out sensitive data or selecting specific fields for an API response.

Array Replacer with Nested Objects

The array replacer applies to all levels of nesting. You must include the keys of nested properties too:

let data = {
name: "Alice",
address: {
city: "Rome",
zip: "00100",
country: "Italy"
}
};

// Must include "address" AND the nested keys you want
let json = JSON.stringify(data, ["name", "address", "city"]);
console.log(json);
// '{"name":"Alice","address":{"city":"Rome"}}'

// Without "address" in the array, the entire nested object is excluded
let json2 = JSON.stringify(data, ["name", "city"]);
console.log(json2);
// '{"name":"Alice"}' ()"city" is nested inside "address" which was excluded)

Replacer as a Function

A replacer function receives each key-value pair and can transform or filter values:

let user = {
name: "Alice",
age: 30,
password: "secret123",
loginCount: 42,
lastLogin: new Date("2024-01-15")
};

let json = JSON.stringify(user, (key, value) => {
// key is "" for the root object itself
if (key === "password") return undefined; // Exclude
if (key === "age") return value + " years"; // Transform
return value; // Keep everything else
});

console.log(json);
// '{"name":"Alice","age":"30 years","loginCount":42,"lastLogin":"2024-01-15T00:00:00.000Z"}'

The replacer function is called for every key-value pair, including nested ones. The first call has an empty string "" as the key, representing the root value:

let data = { a: { b: 2 }, c: 3 };

JSON.stringify(data, (key, value) => {
console.log(`Key: "${key}", Value:`, value);
return value;
});
// Key: "", Value: { a: { b: 2 }, c: 3 } ← root object
// Key: "a", Value: { b: 2 }
// Key: "b", Value: 2
// Key: "c", Value: 3

Practical: Censoring Sensitive Fields

function censorSensitive(key, value) {
const sensitiveKeys = ["password", "token", "secret", "creditCard"];

if (sensitiveKeys.includes(key)) {
return "***REDACTED***";
}
return value;
}

let apiResponse = {
user: "Alice",
token: "eyJhbGciOiJIUzI1NiJ9...",
data: {
creditCard: "4111-1111-1111-1111",
balance: 1000
}
};

console.log(JSON.stringify(apiResponse, censorSensitive, 2));
// {
// "user": "Alice",
// "token": "***REDACTED***",
// "data": {
// "creditCard": "***REDACTED***",
// "balance": 1000
// }
// }

The space Parameter: Pretty Printing

The third argument to JSON.stringify() controls indentation for human-readable output.

Numeric Indentation

let user = { name: "Alice", age: 30, hobbies: ["reading", "coding"] };

// Compact (default)
console.log(JSON.stringify(user));
// '{"name":"Alice","age":30,"hobbies":["reading","coding"]}'

// 2-space indentation (most common)
console.log(JSON.stringify(user, null, 2));
// {
// "name": "Alice",
// "age": 30,
// "hobbies": [
// "reading",
// "coding"
// ]
// }

// 4-space indentation
console.log(JSON.stringify(user, null, 4));
// {
// "name": "Alice",
// "age": 30,
// "hobbies": [
// "reading",
// "coding"
// ]
// }

String Indentation

You can use a string (up to 10 characters) instead of a number:

let data = { a: 1, b: 2 };

console.log(JSON.stringify(data, null, "\t"));
// {
// "a": 1,
// "b": 2
// }

console.log(JSON.stringify(data, null, "-->"));
// {
// -->"a": 1,
// -->"b": 2
// }

Combining Replacer and Space

All three arguments work together:

let user = {
name: "Alice",
password: "secret",
settings: { theme: "dark", lang: "en" }
};

let json = JSON.stringify(
user,
(key, value) => key === "password" ? undefined : value,
2
);

console.log(json);
// {
// "name": "Alice",
// "settings": {
// "theme": "dark",
// "lang": "en"
// }
// }

toJSON(): Custom Serialization

If an object has a toJSON() method, JSON.stringify() calls it and uses its return value instead of the object itself.

How toJSON Works

let meeting = {
title: "Sprint Planning",
date: new Date("2024-01-15T14:30:00Z"),
duration: 60
};

// Date already has a built-in toJSON method:
console.log(meeting.date.toJSON());
// "2024-01-15T14:30:00.000Z"

// This is why dates appear as ISO strings in JSON output
console.log(JSON.stringify(meeting));
// '{"title":"Sprint Planning","date":"2024-01-15T14:30:00.000Z","duration":60}'

Custom toJSON

let room = {
number: 101,
capacity: 20,
equipment: ["projector", "whiteboard", "microphone"],

toJSON() {
return {
id: `room-${this.number}`,
seats: this.capacity,
hasProjector: this.equipment.includes("projector")
};
}
};

console.log(JSON.stringify(room, null, 2));
// {
// "id": "room-101",
// "seats": 20,
// "hasProjector": true
// }

The toJSON method completely replaces the default serialization. The return value is what gets stringified.

toJSON in Nested Objects

let schedule = {
event: "Conference",
room: {
name: "Main Hall",
toJSON() {
return this.name; // Serialize as just the name string
}
}
};

console.log(JSON.stringify(schedule));
// '{"event":"Conference","room":"Main Hall"}'

toJSON Can Return Any JSON-Compatible Value

let counter = {
value: 42,

toJSON() {
return this.value; // Returns a number, not an object
}
};

console.log(JSON.stringify(counter));
// '42'
console.log(JSON.stringify({ count: counter }));
// '{"count":42}'

The toJSON Call Order

When JSON.stringify() encounters an object, it follows this order:

  1. If the object has a toJSON() method, call it and use the return value
  2. Apply the replacer function (if provided) to the result
  3. Recursively stringify the result
let obj = {
value: 42,
toJSON() {
return { transformed: this.value * 2 };
}
};

// toJSON runs first, then replacer
let json = JSON.stringify(obj, (key, value) => {
if (key === "transformed") return value + 1;
return value;
});

console.log(json);
// '{"transformed":85}'
// toJSON: { transformed: 84 } → replacer changes 84 to 85

JSON.parse(): JSON String to Object

JSON.parse() converts a JSON string back into a JavaScript value.

Basic Usage

let json = '{"name":"Alice","age":30,"hobbies":["reading","coding"]}';

let user = JSON.parse(json);
console.log(user.name); // "Alice"
console.log(user.age); // 30
console.log(user.hobbies); // ["reading", "coding"]
console.log(typeof user); // "object"

Parsing Different JSON Values

console.log(JSON.parse('"hello"'));     // "hello" (string)
console.log(JSON.parse('42')); // 42 (number)
console.log(JSON.parse('true')); // true (boolean)
console.log(JSON.parse('null')); // null
console.log(JSON.parse('[1, 2, 3]')); // [1, 2, 3] (array)

Invalid JSON Throws SyntaxError

// All of these throw SyntaxError:

// JSON.parse("undefined"); // undefined is not valid JSON
// JSON.parse("{'name': 'Alice'}"); // Single quotes not allowed
// JSON.parse("{name: 'Alice'}"); // Unquoted keys not allowed
// JSON.parse('{"a": 1,}'); // Trailing comma
// JSON.parse(""); // Empty string
// JSON.parse("hello"); // Unquoted string

Safe Parsing

Always wrap JSON.parse() in a try-catch when parsing external data:

function safeParse(jsonString, fallback = null) {
try {
return JSON.parse(jsonString);
} catch (error) {
console.error("Invalid JSON:", error.message);
return fallback;
}
}

console.log(safeParse('{"valid": true}')); // { valid: true }
console.log(safeParse('not json')); // null
console.log(safeParse('not json', {})); // {} (custom fallback)
console.log(safeParse('not json', [])); // [] (custom fallback)

The Reviver Function: Custom Deserialization

JSON.parse() accepts a second argument called the reviver, a function that transforms each key-value pair during parsing. This is the inverse of the replacer in JSON.stringify().

Basic Reviver

let json = '{"name":"Alice","age":"30","score":"95.5"}';

// Without reviver: all values are strings as stored
let raw = JSON.parse(json);
console.log(typeof raw.age); // "string"

// With reviver: convert numeric strings to numbers
let parsed = JSON.parse(json, (key, value) => {
if (key === "age" || key === "score") return Number(value);
return value;
});

console.log(typeof parsed.age); // "number"
console.log(typeof parsed.score); // "number"
console.log(parsed.age); // 30

Reviving Dates

The most common use of the reviver is converting date strings back to Date objects:

let json = '{"title":"Meeting","date":"2024-01-15T14:30:00.000Z","created":"2024-01-10T09:00:00.000Z"}';

let event = JSON.parse(json, (key, value) => {
// Check if the value looks like an ISO date string
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
let date = new Date(value);
if (!isNaN(date)) return date;
}
return value;
});

console.log(event.date instanceof Date); // true
console.log(event.date.getFullYear()); // 2024
console.log(event.created instanceof Date); // true

Reviver Processing Order

The reviver processes values bottom-up (leaves first, then parents). The last call has the key "" for the root:

let json = '{"a": {"b": 2}, "c": 3}';

JSON.parse(json, (key, value) => {
console.log(`Key: "${key}", Value:`, value);
return value;
});
// Key: "b", Value: 2 ← deepest first
// Key: "a", Value: { b: 2 } ← then parent
// Key: "c", Value: 3
// Key: "", Value: { a: { b: 2 }, c: 3 } ← root last

Excluding Properties with Reviver

Returning undefined from a reviver removes the property:

let json = '{"name":"Alice","password":"secret","age":30,"token":"abc123"}';

let safe = JSON.parse(json, (key, value) => {
if (key === "password" || key === "token") return undefined;
return value;
});

console.log(safe); // { name: "Alice", age: 30 }

Limitations: Functions, undefined, Symbols, Circular References

What JSON Cannot Represent

let complex = {
func: function() { return 42; },
arrow: () => "hello",
undef: undefined,
sym: Symbol("id"),
regex: /pattern/gi,
date: new Date("2024-01-15"),
map: new Map([["a", 1]]),
set: new Set([1, 2, 3]),
nan: NaN,
inf: Infinity,
negInf: -Infinity,
bigint: 42n
};

let json = JSON.stringify(complex);
let parsed = JSON.parse(json);

console.log(parsed);
// {
// regex: {}, ← RegExp becomes empty object
// date: "2024-01-15...", ← Date becomes string
// map: {}, ← Map becomes empty object
// set: {}, ← Set becomes empty object
// nan: null, ← NaN becomes null
// inf: null, ← Infinity becomes null
// negInf: null ← -Infinity becomes null
// }

// func, arrow, undef, sym are completely gone (skipped)
// bigint throws TypeError: Do not know how to serialize a BigInt

Summary of Limitations

Typestringify Behaviorparse Result
FunctionsSkippedMissing
undefinedSkipped (in objects), null (in arrays)Missing / null
SymbolsSkippedMissing
NaNnullnull
Infinity / -Infinitynullnull
DateISO stringString (not Date!)
RegExp{}Empty object
Map / Set{}Empty object
BigIntTypeErrorN/A
Circular referencesTypeErrorN/A

Circular References

Objects that reference themselves (directly or indirectly) cause JSON.stringify to throw:

let obj = { name: "self" };
obj.self = obj; // Circular reference

// TypeError: Converting circular structure to JSON
// JSON.stringify(obj);

let a = {};
let b = { ref: a };
a.ref = b; // Indirect circular reference

// TypeError: Converting circular structure to JSON
// JSON.stringify(a);
BigInt Throws an Error

Unlike other unsupported types that are silently skipped or converted to null, BigInt throws a TypeError when you try to stringify it:

// TypeError: Do not know how to serialize a BigInt
// JSON.stringify({ value: 42n });

// Workaround: convert to string first
let data = { value: 42n };
let json = JSON.stringify(data, (key, value) =>
typeof value === "bigint" ? value.toString() : value
);
console.log(json); // '{"value":"42"}'

Deep Cloning with JSON.parse(JSON.stringify()): Pros and Cons

Before structuredClone() existed, the JSON round-trip was the most common way to create deep clones of objects.

How It Works

let original = {
name: "Alice",
address: {
city: "Rome",
coordinates: { lat: 41.9, lng: 12.5 }
},
hobbies: ["reading", "coding"]
};

let clone = JSON.parse(JSON.stringify(original));

// Deep clone: nested objects are independent
clone.address.city = "Milan";
clone.hobbies.push("hiking");

console.log(original.address.city); // "Rome" (unchanged)
console.log(original.hobbies); // ["reading", "coding"] (unchanged)

Pros

  • Works in every JavaScript environment (no polyfills needed)
  • Simple one-line syntax
  • Handles deeply nested structures
  • Well understood and widely used

Cons: Data Loss

The JSON round-trip silently transforms or drops data:

let original = {
name: "Alice",
createdAt: new Date("2024-01-15"),
pattern: /test/gi,
callback: () => console.log("hi"),
nothing: undefined,
count: NaN,
tags: new Set(["a", "b"]),
metadata: new Map([["key", "value"]])
};

let clone = JSON.parse(JSON.stringify(original));

console.log(clone.createdAt); // "2024-01-15T00:00:00.000Z" (STRING, not Date!)
console.log(clone.createdAt instanceof Date); // false
console.log(clone.pattern); // {} (empty object, not RegExp!)
console.log(clone.callback); // undefined (function is gone!)
console.log("nothing" in clone); // false (undefined property removed!)
console.log(clone.count); // null (NaN became null!)
console.log(clone.tags); // {} (Set became empty object!)
console.log(clone.metadata); // {} (Map became empty object!)

Cons: Fails on Circular References

let obj = { name: "circular" };
obj.self = obj;

// TypeError: Converting circular structure to JSON
// JSON.parse(JSON.stringify(obj));

structuredClone() vs. JSON for Deep Cloning

structuredClone() (available in modern browsers and Node.js 17+) is the modern alternative for deep cloning.

Comparison

let original = {
name: "Alice",
date: new Date("2024-01-15"),
data: new Map([["key", "value"]]),
tags: new Set([1, 2, 3]),
buffer: new ArrayBuffer(8),
pattern: /test/gi
};

// JSON method: loses type information
let jsonClone = JSON.parse(JSON.stringify(original));
console.log(jsonClone.date instanceof Date); // false (it's a string)
console.log(jsonClone.data instanceof Map); // false (it's {})
console.log(jsonClone.tags instanceof Set); // false (it's {})

// structuredClone: preserves types
let structClone = structuredClone(original);
console.log(structClone.date instanceof Date); // true ✅
console.log(structClone.data instanceof Map); // true ✅
console.log(structClone.tags instanceof Set); // true ✅
console.log(structClone.pattern instanceof RegExp); // true ✅

Circular References

let obj = { name: "circular" };
obj.self = obj;

// JSON: throws TypeError
// JSON.parse(JSON.stringify(obj));

// structuredClone: handles it correctly
let clone = structuredClone(obj);
console.log(clone.self === clone); // true (circular structure preserved)
console.log(clone.self === obj); // false (different object)

Complete Comparison Table

FeatureJSON.parse(JSON.stringify())structuredClone()
Nested objects/arraysYesYes
DateBecomes stringPreserved
Map / SetBecomes {}Preserved
RegExpBecomes {}Preserved
ArrayBuffer / TypedArrayLoses dataPreserved
Circular referencesThrows errorHandled
FunctionsSilently droppedThrows error
SymbolsSilently droppedThrows error
undefined valuesDropped/nullPreserved
NaN / InfinityBecomes nullPreserved
Prototype chainLostLost
DOM nodesN/ACloneable
PerformanceModerateGenerally faster
Browser supportUniversalModern browsers, Node 17+

When to Use Which

// ✅ Use structuredClone() for deep cloning
let clone1 = structuredClone(complexObject);

// ✅ Use JSON when you specifically need a JSON string
let jsonString = JSON.stringify(data);
// ... send over network, save to file, etc.
let parsed = JSON.parse(jsonString);

// ✅ Use JSON round-trip only for simple, JSON-safe data in legacy environments
let simpleClone = JSON.parse(JSON.stringify({ a: 1, b: "hello", c: [1, 2] }));
Default to structuredClone()

For deep cloning, always prefer structuredClone() in modern environments. It handles more types correctly, supports circular references, and is generally more performant. Use the JSON method only when you need to support very old environments or when you specifically need JSON serialization.

Practical Patterns

Storing and Retrieving from localStorage

// Save to localStorage
let settings = { theme: "dark", fontSize: 16, language: "en" };
localStorage.setItem("settings", JSON.stringify(settings));

// Retrieve from localStorage
let stored = localStorage.getItem("settings");
let parsed = stored ? JSON.parse(stored) : { theme: "light", fontSize: 14, language: "en" };
console.log(parsed.theme); // "dark"

API Communication

// Sending JSON to an API
async function createUser(userData) {
let response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userData) // Object → JSON string
});

let result = await response.json(); // JSON string → Object (built-in)
return result;
}

Configuration Files

// Reading a JSON config file (Node.js)
// import config from "./config.json" assert { type: "json" };

// Or manually:
// let configText = fs.readFileSync("./config.json", "utf-8");
// let config = JSON.parse(configText);

Pretty Printing for Debugging

let complexData = {
users: [
{ name: "Alice", roles: ["admin", "user"] },
{ name: "Bob", roles: ["user"] }
],
settings: { debug: true, version: "2.0" }
};

// Quick way to inspect complex objects
console.log(JSON.stringify(complexData, null, 2));

Summary

  • JSON is a text-based data exchange format that supports strings, numbers, booleans, null, objects, and arrays. Property names must be double-quoted strings. No trailing commas, comments, or JavaScript-specific types.
  • JSON.stringify(value, replacer, space) converts JavaScript values to JSON strings. Functions, undefined, and symbols are silently skipped. Dates become ISO strings. NaN and Infinity become null.
  • The replacer (second argument) controls serialization: pass an array of property names to include, or a function to transform/filter values.
  • The space (third argument) controls indentation. Use 2 for readable output, null or omit for compact output.
  • toJSON() on an object lets you customize what gets serialized. The return value replaces the entire object in the output.
  • JSON.parse(string, reviver) converts JSON strings back to JavaScript values. Always wrap in try-catch when parsing external data.
  • The reviver (second argument) transforms values during parsing. Essential for converting date strings back to Date objects.
  • JSON cannot represent functions, undefined, symbols, RegExp, Map, Set, BigInt, or circular references. BigInt and circular references throw errors; others are silently lost or transformed.
  • JSON.parse(JSON.stringify()) for deep cloning works for simple, JSON-safe data but loses type information for Dates, Maps, Sets, and other non-JSON types.
  • structuredClone() is the modern alternative for deep cloning. It preserves Dates, Maps, Sets, RegExp, handles circular references, and is generally preferred over the JSON method.