TechEarl

How to Merge Two Arrays in JavaScript

Merge two arrays in JavaScript three ways: copy-merge with spread [...a, ...b], merge in place with a.push(...b), or copy with a.concat(b). Which to pick by mutation, memory, and the spread arg-count limit that bites on very large arrays.

Ishan Karunaratne⏱️ 7 min readUpdated
Share thisCopied
Merge two arrays in JavaScript: copy-merge with spread, merge in place with push, or copy with concat, and the choice by mutation, memory, and large-array safety.

The fastest answer: to merge two arrays into a new array, use spread, const merged = [...a, ...b]. To merge b into an existing array a without allocating a new one, use a.push(...b). To get a copy without touching the originals and without the spread caveat, use a.concat(b). All three are correct in 2026; the right pick depends on whether you want to mutate, how big the arrays are, and how much memory you can spend.

javascript
const a = [1, 2, 3];
const b = [4, 5, 6];

const merged = [...a, ...b];   // [1, 2, 3, 4, 5, 6], new array, a and b untouched
a.push(...b);                  // a is now [1, 2, 3, 4, 5, 6], b untouched
const copy = a.concat(b);      // new array, a and b untouched (a is still the pushed version above)

Copy-merge with spread: [...a, ...b]

This is the one I reach for by default. [...a, ...b] builds a brand new array by expanding each source into a fresh array literal. The originals are never modified, the order is exactly a then b, and it reads cleanly at a glance.

javascript
const front = ["a", "b"];
const back = ["c", "d"];
const all = [...front, ...back]; // ["a", "b", "c", "d"]

It also composes naturally with more than two sources and with single values mixed in: [...front, "x", ...back]. The cost is that it allocates a new array of a.length + b.length, so for a moment you are holding the originals plus the merged result in memory. For ordinary arrays that is a non-issue. For very large arrays it matters, and there is a hard limit worth knowing about (below).

Merge in place with a.push(...b)

When you already own a and want b's elements appended to it, a.push(...b) mutates a directly and returns the new length. No second array is allocated for the result, which is the memory win.

javascript
const log = ["start"];
const newEntries = ["step 1", "step 2"];
log.push(...newEntries); // log is now ["start", "step 1", "step 2"]

This is the modern replacement for the old Array.prototype.push.apply(a, b) trick you still see in pre-2015 code; spread does the same thing and reads better. Use it only when mutating a is actually what you want. If a is shared (state someone else holds a reference to, a prop, a frozen-by-convention constant), mutating it in place is a bug magnet, and you should copy-merge instead.

Copy with a.concat(b)

concat returns a new array and leaves both originals alone, same end result as spread but a different shape. Its one genuine behavioral wrinkle is that it flattens one level of any array argument: array args get spread in, non-array args get appended as-is.

javascript
[1, 2].concat([3, 4]);   // [1, 2, 3, 4]   array arg is flattened one level
[1, 2].concat(3, 4);     // [1, 2, 3, 4]   plain values appended
[1, 2].concat([[3, 4]]); // [1, 2, [3, 4]] only ONE level is flattened

That one-level behavior is exactly why concat is the safe choice for very large arrays: it does not pass b's elements as function arguments the way spread does, so it sidesteps the arg-count limit described next.

The spread arg-count gotcha (very large arrays)

a.push(...b) and f(...b) expand b into individual function arguments. JavaScript engines cap how many arguments a single call can take, because the spread elements land on the call stack. In V8 (Chrome, Node.js) that ceiling sits around 125,000 arguments, though it varies with the engine, version, and available stack; once b is large enough, push(...b) throws a RangeError: Maximum call stack size exceeded instead of merging. Treat anything in the tens of thousands as the danger zone rather than waiting for the exact limit.

javascript
const huge = new Array(200_000).fill(0);
const target = [];
target.push(...huge); // RangeError on V8: too many spread arguments overflow the call stack

The fix is to not spread at all for large arrays. Either loop, or use concat, neither of which routes the elements through an argument list:

javascript
// Loop append, in place, no arg-count risk:
function te_extend(target, source) {
  for (let i = 0; i < source.length; i++) target.push(source[i]);
  return target;
}

// Or copy-merge without spread:
const merged = a.concat(b); // safe at any size

I treat "the arrays might be tens of thousands of elements or larger" as the trigger to drop spread and reach for concat (copy) or a loop (in place). For everyday arrays, spread is fine and clearer.

Which one should I use?

TechniqueMutates?Allocates new array?Safe for very large arrays?Reach for it when
[...a, ...b]NoYesNo (arg-count limit)You want a new merged array; readability matters
a.push(...b)Yes (a)NoNo (arg-count limit)You own a and want it extended in place
a.concat(b)NoYesYesYou want a copy, or the arrays are very large
loop pushYes (a)NoYesIn-place merge of very large arrays

The decision is really two questions. First, do you want to mutate an existing array or produce a new one? Mutating points at push; a new array points at spread or concat. Second, are the arrays large enough to risk the arg-count limit? If yes, avoid anything that spreads the source into a call: use concat for a copy or a loop for in place.

One thing none of these do is deduplicate. Merging [1, 2] and [2, 3] gives [1, 2, 2, 3], not [1, 2, 3]. If you want unique values after merging, see removing duplicates from an array. And if you are reaching for the full set of modern array methods (mutating versus copying variants, flat, at), merging is one corner of a larger toolkit. For objects rather than arrays, the spread idea carries over but the rules differ; see merging objects in JavaScript.

For a new array, [...a, ...b] (spread) is the cleanest and is the default choice. To append b onto an existing array without allocating a new one, a.push(...b) mutates a in place, which uses less memory. Both are fast for ordinary arrays.

No. Both [...a, ...b] and a.concat(b) return a new array and leave the originals untouched. Only a.push(...b) (and a loop that calls push) modifies the target array in place.

Spread expands the source into individual function arguments, and engines cap how many arguments one call can take (tens of thousands in V8). Past that, push(...big) throws RangeError: Maximum call stack size exceeded. For very large arrays, use a.concat(b) for a copy or loop with push for an in-place merge, since neither routes elements through an argument list.

Only one level. [1, 2].concat([3, 4]) gives [1, 2, 3, 4], but [1, 2].concat([[3, 4]]) keeps the inner array: [1, 2, [3, 4]]. To flatten deeper, use arr.flat(depth) or arr.flat(Infinity).

Merge first, then dedupe: [...new Set([...a, ...b])] works for primitives. For arrays of objects, dedupe by a key with a Map. See removing duplicates from an array for the object case and the Set semantics.

See also

Sources

Authoritative references this article was fact-checked against.

TagsJavaScriptarraysspread operatorconcatpushES6

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Software Systems Architect · Senior Software Engineer · Engineering Leadership

Software systems architect and senior software engineer with more than two decades designing, building, and running production software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Now a CTO, though what I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

JavaScript has no blocking sleep. Await a Promise around setTimeout, use Node's native timers/promises, poll for a condition with AbortSignal, and know the setTimeout 4ms clamp and Date.now vs performance.now difference.

How to sleep(), wait, and poll in JavaScript

JavaScript has no blocking sleep(). Here is the one-liner that actually works (await a Promise around setTimeout), Node's native timers/promises, a cancellable polling helper, and the setTimeout 4ms-clamp and Date.now vs performance.now gotchas.

How async functions work in JavaScript: they desugar to a generator plus a runner. Plus async generators with for await...of, the AsyncFunction constructor, and why you should not detect async-ness.

How async Functions Really Work in JavaScript

What an async function actually is under the hood: it desugars to a generator plus a built-in runner. Plus async generators with for await...of, the AsyncFunction constructor, and why detecting async-ness is a trap.

How to store an array in PostgreSQL: native array column types declared with [], the curly-brace and ARRAY[...] literal forms, containment and overlap queries with @> and &&, ANY() membership, and a GIN index.

How to Store an Array in PostgreSQL

PostgreSQL has native array types: any base type can be an array, declared with []. How to insert with the curly-brace literal or ARRAY[...], query with @>, &&, and ANY(), index with GIN, and when an array beats a junction table or jsonb.