Mid-level 3 min · March 05, 2026

Spread Operator — Shallow Merge That Killed User Settings

Shallow spread from `{.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Spread unpacks iterables into individual values; rest collects remaining values into a real array.
  • Spread creates shallow copies — nested objects are shared references, not clones.
  • Rest must be the last parameter; it's the only way to get a true Array from variadic arguments.
  • Performance cost: spreading large arrays (10k+ items) blocks the event loop ~0.5ms per 10k.
  • Production trap: merging nested configs with spread silently overrides deeper keys instead of merging them.
  • Biggest mistake: assuming spread deep-clones — it doesn't, causing mutation bugs in state management.
Plain-English First

Imagine you ordered a pizza and want to share each slice individually — you 'spread' the pizza across plates. Now imagine a waiter collecting all leftover slices into one box — that's 'rest'. The three dots (...) in JavaScript do exactly this: spread unpacks a collection into individual pieces, and rest gathers individual pieces back into a collection. Same symbol, opposite jobs — context decides which one you're using.

Every modern JavaScript codebase is full of three little dots — ... — and for good reason. Whether you're merging objects in a Redux reducer, passing dynamic arguments to a function, or cloning arrays without mutating the original, the spread and rest operators are doing the heavy lifting. They're not just syntactic sugar; they fundamentally change how you think about data flow in JavaScript.

Before ES6 introduced these operators, copying arrays meant using .slice(), merging objects required Object.assign(), and handling variable-length function arguments meant wrestling with the awkward arguments object. That code was verbose, error-prone, and frankly hard to read at a glance. The ... syntax replaced all of that with something readable enough that your intent is obvious the moment someone looks at your code.

By the end of this article you'll know the difference between spread and rest at a conceptual level (not just syntactically), you'll recognise the real-world patterns where each one shines, you'll understand the gotchas that trip up even experienced developers, and you'll be ready to answer the interview questions that separate candidates who 'know the syntax' from those who actually understand JavaScript.

Spread Operator: Unpacking Collections Where You Need Individual Items

The spread operator takes an iterable — an array, a string, a Set, or any object with a Symbol.iterator — and expands it in-place. Think of it as opening a bag and tipping everything out onto the table.

The most important word there is 'in-place'. Spread doesn't return anything on its own; it works by expanding values into the surrounding context. That context might be an array literal, a function call, or an object literal — and each context behaves slightly differently.

When you spread inside an array literal [...a, ...b], you're building a new array from the elements of both. This is a shallow copy — primitives are copied by value, but nested objects are still referenced. When you spread inside a function call Math.max(...scores), you're passing each element as a separate argument. When you spread inside an object literal { ...defaults, ...overrides }, later keys win, which makes it perfect for configuration merging.

The key insight: spread is about placement. You're deciding exactly where the unpacked values land, and that precision is what makes it so powerful for immutable update patterns.

io/thecodeforge/syntax/SpreadPatterns.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * io.thecodeforge - Mastering the Spread Operator
 */

// 1. Combining arrays without mutating either original
const morningTasks = ['email', 'standup', 'code review'];
const afternoonTasks = ['pair programming', 'deploy', 'retrospective'];
const fullDaySchedule = [...morningTasks, 'lunch', ...afternoonTasks];

// 2. Passing array values as individual function arguments
const temperatures = [22, 19, 31, 28, 17, 25];
const hottest = Math.max(...temperatures);

// 3. Shallow-cloning an array (avoids accidental mutation)
const originalCart = [{ id: 1, name: 'Keyboard' }, { id: 2, name: 'Mouse' }];
const cartCopy = [...originalCart];
cartCopy.push({ id: 3, name: 'Monitor' });

// 4. Merging config objects — later keys override earlier ones
const defaultSettings = { theme: 'light', fontSize: 14, notifications: true };
const userSettings    = { theme: 'dark', fontSize: 16 };
const finalSettings = { ...defaultSettings, ...userSettings };

console.log(finalSettings);
Output
{ theme: 'dark', fontSize: 16, notifications: true }
Watch Out: Spread Is a Shallow Copy
When you spread an array of objects, the objects themselves are NOT cloned — you get new references to the same objects. Mutating cartCopy[0].name = 'Trackpad' would also change originalCart[0].name. Use structuredClone() for deep copies.
Production Insight
In Redux reducers, using spread to update nested state often introduces subtle mutation bugs because reducers may accidentally share references.
If you're building a form wizard with deeply nested state, spread alone is insufficient — you need Immer or manual deep updates.
Rule: when state depth > 2, don't rely on spread — use a library or structuredClone.
Key Takeaway
Spread is a shallow copy only.
Use for flat objects and arrays.
Never trust it for deep nesting — always verify with a unit test.

Rest Parameters: Capturing 'Everything Else' Into One Clean Collection

Rest is the mirror image of spread. Where spread expands, rest collects. Its job is to gather up a variable number of values and bundle them into a real JavaScript array that you can then work with normally.

The critical word is 'real array'. Before rest parameters existed, functions used the arguments object to access extra arguments. arguments looks like an array but isn't — it has no .map(), no .filter(), no .reduce(). Developers constantly had to convert it. Rest parameters made that hack obsolete.

Rest must always be the last parameter in a function signature. Importantly, rest only collects arguments that don't have an explicit parameter waiting for them. Named parameters come first, then rest catches whatever remains.

io/thecodeforge/syntax/RestPatterns.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * io.thecodeforge - Mastering Rest Parameters
 */

// 1. Basic rest parameter: collect unlimited arguments
function calculateOrderTotal(discountPercent, ...itemPrices) {
  const subtotal = itemPrices.reduce((sum, price) => sum + price, 0);
  const discount = subtotal * (discountPercent / 100);
  return (subtotal - discount).toFixed(2);
}

// 2. Rest in object destructuring — extract known keys, collect the rest
const { id, createdAt, ...publicProfile } = {
  id: 'usr_abc123',
  createdAt: '2023-06-01',
  username: 'jsmith',
  bio: 'Software engineer'
};

console.log(publicProfile);
Output
{ username: 'jsmith', bio: 'Software engineer' }
Pro Tip: Strip Sensitive Fields With Object Rest
The object destructuring rest pattern (const { password, token, ...safeUser } = userData) is one of the cleanest ways to remove sensitive fields before passing data down to a UI or returning it from an API.
Production Insight
In Node.js REST handlers, using rest to strip fields from request body is common but dangerous if you don't validate the incoming keys — a malicious actor could inject extra fields.
Rest loses the prototype chain: you'll get a plain object, not an instance of a custom class.
Rule: always sanitise the rest object before passing it to the next layer.
Key Takeaway
Rest gives you a real Array — map, filter, reduce work immediately.
Use for variadic functions and object destructuring.
Remember: rest is the opposite of spread — it gathers, not expands.

The Senior Level: Forwarding and Higher-Order Patterns

In production, rest and spread are often used together to create 'Transparent Wrappers'. This pattern allows you to intercept a function call, add logic (like logging or timing), and then forward the exact original arguments to the underlying function without knowing what those arguments are.

io/thecodeforge/patterns/ArgumentForwarding.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * io.thecodeforge - Transparent Function Wrapping
 */
function withLogging(fn) {
  return function(...args) {
    console.log(`[FORGE-LOG] Calling ${fn.name} with:`, args);
    return fn(...args); // Forwarding args via spread
  };
}

const add = (a, b) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(5, 10);
Output
[FORGE-LOG] Calling add with: [5, 10]
Production Insight
When using rest to forward arguments in middleware chains, be careful with this binding — arrow functions capture this lexically, so if you use rest in an arrow wrapper, you may lose the expected context.
Performance notice: creating a new rest array on every invocation has a small overhead; for hot functions (called 10k+ times/sec), consider using a single-use wrapper or inline the logging.
Rule: use rest+spread forwarding for wrappers, but benchmark if performance is critical.
Key Takeaway
Rest collects current arguments; spread forwards them.
Transparent wrappers are clean with rest+spread.
Watch this binding and hot-path performance.

Performance and Memory: When Spread Costs You

Spread is elegant, but it's not free. Every ...array call allocates a new array and copies all elements. For small arrays it's negligible, but if you're spreading a 100k-item log array in a hot loop, you'll see GC pressure and jank.

Object spread is even more subtle: it calls Object.assign() under the hood and enumerates all own properties. If the object has getters or a heavy prototype chain, those getters execute during spread — leading to side effects you didn't expect.

V8 optimises spreads for simple cases, but the allocation still happens. For immutable updates in React, the framework relies on reference identity, so spreading every render can create new objects that trigger unnecessary re-renders if not memoised.

io/thecodeforge/performance/SpreadAllocation.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * io.thecodeforge - Measuring spread allocation cost
 */
// Simulate a large array spread
function processLargeLog(entries) {
  // This creates a new array copy every call
  const processed = [...entries, { timestamp: Date.now(), action: 'check' }];
  return processed;
}

// Better: push to existing array if memory is critical
function processLargeLogOptimized(entries) {
  entries.push({ timestamp: Date.now(), action: 'check' });
  return entries;
}

console.time('spread');
const large = Array(50000).fill(0).map((_, i) => ({ id: i }));
processLargeLog(large);
console.timeEnd('spread'); // ~2-3ms on typical hardware
Output
spread: 2.34ms
V8 Optimisations
Modern V8 (Node 18+) can inline simple spread patterns and avoid allocations in some cases, but don't rely on that — profile your bottlenecks. If you see high GC activity around spread usage, consider replacing with direct mutations inside a local scope.
Production Insight
A log aggregator service that used spread to prepend a timestamp to each log entry was causing 300ms garbage collection pauses every 30 seconds under load. Replacing spread with unshift reduced pauses to 15ms.
Object spread inside a Redux selector that was called every 16ms caused the selector to always return a new reference, breaking useSelector memoisation and flooding the UI with re-renders.
Rule: never spread inside a selector or a hot loop — use useMemo or manual reference equality.
Key Takeaway
Spread allocates memory — big arrays cost big GC.
Object spread triggers getters and loses reference identity.
Profile before optimising, but be aware: spread is not free.

Edge Cases: Sparse Arrays, Strings, and Iterables

Spread works on any iterable, but iterables aren't always arrays. Strings are iterable — [...'hello'] gives ['h','e','l','l','o']. Great for splitting. But emoji? [...'👍'] returns ['👍'] correctly because strings are Unicode-aware. That's better than .split('') which can break multi-byte characters.

Sparse arrays are tricky. Array(5).fill() creates a dense array, but Array(5) alone leaves holes. Spreading a sparse array preserves the holes as undefined? Actually, [...Array(5)] returns [undefined, undefined, undefined, undefined, undefined] — the holes become undefined. That's a change! If you need to preserve sparseness, use Array.from().

Another gotcha: arguments is not an iterable in strict mode? Actually arguments is iterable in ES2015+, but still not an array. Spread works: [...arguments]. But better to just use rest parameters.

io/thecodeforge/syntax/EdgeCases.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * io.thecodeforge - Edge Cases of Spread
 */
// Unicode handling
const emojiString = '👋🌍';
console.log([...emojiString]); // ['👋', '🌍']
console.log(emojiString.split('')); // ['\uD83D', '\uDC4B', ...] broken!

// Sparse arrays
const sparse = Array(3);
console.log(sparse.length); // 3
console.log([...sparse]); // [undefined, undefined, undefined] (holes become undefined)

// Non-iterable object
const obj = { a: 1 };
// [...obj]; // TypeError: obj is not iterable

// Set
const set = new Set([1, 2, 3]);
console.log([...set]); // [1, 2, 3]
Output
['👋', '🌍']
[undefined, undefined, undefined]
[1, 2, 3]
Spread = For-of loop unrolled
  • Spread loops over the iterable using its [Symbol.iterator] method.
  • Each iteration yields a value; spread places it into the new array/object.
  • If the object is not iterable, you get a TypeError.
  • Strings, Set, Map, NodeList are iterable. Plain objects are not (use Object.keys() first).
  • Generator functions return an iterable, so [...someGenerator()] works.
Production Insight
A data pipeline that used spread on a Set lost duplicate information that was intentionally stored as a Set property. The spread converted the Set to an array of values, discarding the uniqueness constraints.
When receiving data from an API, some arrays were sparse (e.g., [1, , 3]). Spreading that array produced [1, undefined, 3] which passed validation incorrectly and caused downstream crashes.
Rule: always test with edge inputs — empty strings, sparse arrays, Sets, and custom iterables.
Key Takeaway
Spread respects the iteration protocol.
Works well with Strings, Set, Map — but not plain objects.
Sparse arrays become dense with undefined holes — use Array.from for sparse preservation.
● Production incidentPOST-MORTEMseverity: high

The Config Merge That Silently Killed User Settings

Symptom
Users reported that their advanced display settings (e.g., colour profile, layout grid) randomly reverted to defaults after updating any other setting.
Assumption
The team assumed { ...defaults, ...userOverrides } would deep-merge the nested objects, preserving all nested keys.
Root cause
Spread performs shallow copying: nested objects inside defaults and userOverrides are replaced entirely, not merged. The displayPreferences object in userOverrides clobbered the corresponding object in defaults, dropping any keys not explicitly set by the user.
Fix
Replace shallow spread with a deep merge function (e.g., using structuredClone on defaults and then merging overrides with a custom deep assign). For React state, ensure each nested level is spread independently.
Key lesson
  • Spread only copies one level deep — always deep-merge nested configs for user preferences.
  • Add a unit test that verifies nested object keys survive a merge.
  • Use "shallow" as a mental tag every time you write {...a, ...b}.
Production debug guideDiagnose and fix the most common runtime problems caused by incorrect use of ...4 entries
Symptom · 01
State mutation after spread – UI not re-rendering in React
Fix
Check if the spread was applied to a nested object without copying inner levels. Use JSON.parse(JSON.stringify(obj)) or structuredClone() for full isolation.
Symptom · 02
Function receives extra undefined arguments after rest pattern
Fix
Ensure rest parameter is the last in the function signature. Log the rest array length to verify no accidental undefined values from missing arguments.
Symptom · 03
Array spread introduces sparse holes (empty slots)
Fix
Verify source array has no empty items (e.g., from Array(5)). Use Array.from() instead of spread to convert sparse arrays.
Symptom · 04
Object spread does not preserve prototype chain
Fix
If you need to keep prototype methods (e.g., custom class instances), avoid spread. Use Object.assign() with a target that has the correct prototype, or use a factory method.
★ Quick Debug Cheat Sheet for Spread/RestWhen something breaks and you suspect the ... operator, run these commands.
Nested object merged incorrectly
Immediate action
Log the source objects to confirm key overlap.
Commands
console.log('defaults:', JSON.stringify(defaults, null, 2))
console.log('userOverrides:', JSON.stringify(userOverrides, null, 2))
Fix now
Replace {...defaults, ...userOverrides} with a deep merge function. For quick patch: JSON.parse(JSON.stringify(defaults)) then spread overrides.
Array spread behaves unexpectedly with non-iterable+
Immediate action
Check if the value is actually iterable by wrapping in `Array.from()` or using `typeof value[Symbol.iterator]`.
Commands
console.log('is iterable?', typeof value[Symbol.iterator])
console.log('type:', typeof value)
Fix now
Convert to array first: const arr = [].concat(value) or use Array.isArray() guard before spreading.
Rest parameter not collecting all arguments+
Immediate action
Verify the function signature – rest must be the last parameter. Check if you accidentally used `arguments` inside an arrow function.
Commands
console.log('function length:', myFunc.length)
console.log('rest array:', myRestParam)
Fix now
Move rest parameter to the end. For arrow functions, use rest directly; arguments is not available.
Spread vs Rest at a Glance
Feature / AspectSpread (...)Rest (...)
What it doesExpands one iterable into many individual valuesCollects many individual values into one array
Position in codeRight side of assignment, inside literals, in function callsLeft side of destructuring, in function parameter lists
Input typeAny iterable (array, string, Set, Map, object)Individual values / arguments
Output typeIndividual values placed into surrounding contextA true JavaScript Array
Can appear multiple timesYes — spread multiple sources in one expressionNo — only one rest element per function/destructuring
Must be last?No — spread can appear anywhere in the listYes — rest must always be the last parameter

Key takeaways

1
Same syntax ..., opposite roles
spread expands one thing into many; rest collects many things into one.
2
Spread creates shallow copies
nested objects are still shared references.
3
Rest parameters produce a real JavaScript Array, allowing immediate use of .map(), .filter(), and .reduce().
4
In React, object spread is the standard for immutable state updates to ensure re-renders trigger correctly.
5
Performance red flag
large spreads in hot paths cause GC pressure — always profile.
6
Edge cases
sparse arrays, emoji strings, sets — test them before you ship.

Common mistakes to avoid

5 patterns
×

Spreading an object into an array

Symptom
[...obj] throws a TypeError because plain objects are not iterable by default.
Fix
Use Object.keys(), Object.values() or Object.entries() first to convert the object into an iterable.
×

Expecting spread to deep-clone

Symptom
Mutating a nested property in the spread copy changes the original object, causing data corruption in production.
Fix
For deep cloning, use structuredClone(obj) in modern environments. For older platforms, JSON.parse(JSON.stringify(obj)) or a utility like Lodash cloneDeep.
×

Placing rest parameter before others

Symptom
SyntaxError: Rest parameter must be last formal parameter at script parse time.
Fix
Always put the rest parameter as the final parameter: function process(first, ...rest) — never before a named parameter.
×

Using spread on arguments inside an arrow function

Symptom
arguments is not available in arrow functions. [...arguments] throws a ReferenceError.
Fix
Use rest parameters directly in the arrow function signature: const myArrow = (...args) => args.
×

Assuming object spread merges arrays

Symptom
{ ...a, ...b } where both a and b have an array property — the result contains only the array from b, lost all items from a.
Fix
Manually merge arrays: { ...a, items: [...(a.items || []), ...(b.items || [])] }.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain why `const arr2 = [...arr1]` is safer than `const arr2 = arr1` i...
Q02JUNIOR
Write a function that uses rest parameters to accept any number of numer...
Q03SENIOR
In the context of object destructuring, how does the rest operator help ...
Q04SENIOR
What happens when you use the spread operator on a string, and how does ...
Q05SENIOR
Can you use spread with a Map? What does `[...myMap]` produce?
Q01 of 05SENIOR

Explain why `const arr2 = [...arr1]` is safer than `const arr2 = arr1` in a React application.

ANSWER
arr2 = arr1 creates a reference copy; mutating arr2 also mutates arr1, which can cause side effects in React's reconciliation. Spread creates a new array reference, ensuring the original array is not mutated. This is crucial for immutability in state management.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Can I use the spread operator to deep clone an object in JavaScript?
02
What's the difference between the rest parameter and the `arguments` object?
03
Why does the order of objects matter when using spread to merge them?
04
Is it possible to spread a `NodeList` (e.g., from `document.querySelectorAll`)?
05
What is the performance impact of using rest parameters in a function called thousands of times per second?
🔥

That's Advanced JS. Mark it forged?

3 min read · try the examples if you haven't

Previous
Destructuring in JavaScript
8 / 27 · Advanced JS
Next
Arrow Functions in JavaScript