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.

See also

Sources

Authoritative references this article was fact-checked against.

TagsJavaScriptarraysspread operatorconcatpushES6

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

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 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

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.