Devpuff
Pricing
Log InStart Free
  1. Home
  2. Blog
  3. How to merge two arrays in JavaScript
Learn

How to merge two arrays in JavaScript

To merge arrays in JavaScript, spread and concat both work. push(...arr) throws a RangeError at 200,000 items, which is why the choice matters.

By Max Arthur
Co-Founder & Content Marketer·September 1, 2026·9 Min read
Two arrays combined by spread into a new array, beside push spreading into an existing one and overflowing the stack

Two ways, both fine:

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

[...a, ...b];    // [1, 2, 3, 4]
a.concat(b);     // [1, 2, 3, 4]

Both return a new array and leave the originals untouched. Measured, a was still [1, 2] after each.

The third option looks equivalent and is not:

a.push(...b);    // mutates a, and breaks on large arrays

Spread or concat

They do the same job, and the differences are small enough that either is defensible.

Spread reads better with more than two arrays and mixes literals in naturally:

[...a, ...b, ...c];
[...a, 99, ...b];

concat handles non-arrays without ceremony:

[1, 2].concat(5);   // [1, 2, 5]

Measured, that appends 5 rather than erroring or spreading it. Spread would need [...a, 5], which is equally short.

Neither flattens nested arrays:

[...[1, [2]], ...[3]];   // [1, [2], 3]

The inner [2] stays an array. Both methods go exactly one level deep, which is what you want when the elements are themselves arrays and a surprise when you expected flattening.

For actual flattening:

[1, [2, [3]]].flat();          // [1, 2, [3]]
[1, [2, [3]]].flat(Infinity);  // [1, 2, 3]

flat() defaults to one level. flat(Infinity) goes all the way down, and is the tidy way to collapse an arbitrarily nested structure.

The push trap

push(...b) appends b's items to a in place, which is genuinely useful when you want to mutate. It also has a hard limit:

const huge = new Array(200000).fill(0);
const dest = [];
dest.push(...huge);
RangeError: Maximum call stack size exceeded

Two hundred thousand items is enough to break it. Spreading into a function call passes every element as a separate argument, and engines cap how many arguments a call can take. It is the call stack that overflows, not memory.

The failure is abrupt and depends on data size, so it passes every test written against a small fixture and fails on a real dataset. If you need to append in place at scale, loop:

for (const item of huge) dest.push(item);

Or build a new array with spread, which has no such limit because it is not a function call:

const dest = [...dest, ...huge];   // fine at any size

That asymmetry is worth remembering: [...a, ...b] is safe at any size, a.push(...b) is not.

Merging without duplicates

A Set cannot hold the same value twice, so combining and deduplicating is one expression:

[...new Set([...a, ...b])];   // [1, 2, 3]

Measured on [1, 2] and [2, 3], that gives [1, 2, 3] with the shared 2 appearing once. Order is preserved, and the first occurrence is the one kept.

This only works on primitives. Objects compare by reference, so two identical-looking objects both survive, and the fix is a Map keyed on a field. The fix there is the Map approach in the next section.

Merging arrays of objects by a key

A common real task: two lists of the same records, and later data should win.

const merged = [...new Map(
  [...a, ...b].map((item) => [item.id, item])
).values()];

Build a Map keyed on id, and later entries overwrite earlier ones because that is how Map.set behaves. Since b comes second in the spread, b's version of any shared id survives.

Swap the order to make a win. That ordering is the entire behaviour, and it is worth a comment in the code because it is not obvious to the next reader.

To merge the objects themselves rather than replacing them, spread both:

const byId = new Map(a.map((i) => [i.id, i]));
for (const item of b) {
  byId.set(item.id, { ...byId.get(item.id), ...item });
}

Now shared ids keep fields from a that b does not have. That is usually what "merge" means when the two sources are partial records.

Interleaving, and other shapes

Occasionally you want something other than one list after the other.

// pairwise, using the shorter length
a.map((x, i) => [x, b[i]]);

// alternating
a.flatMap((x, i) => [x, b[i]]);

flatMap is map followed by a one-level flat, which makes alternating a one-liner. Both stop making sense when the arrays differ in length, so check that first if the inputs are not guaranteed to match.

Which to use

Situation Use
Combine two arrays [...a, ...b]
Combine and keep the original a.concat(b), same thing
Append in place, small arrays a.push(...b)
Append in place, large arrays a for...of loop
Combine and deduplicate primitives [...new Set([...a, ...b])]
Combine objects by id a Map keyed on the field
Flatten nested arrays flat() or flat(Infinity)

Merging objects is a different operation

The same spread syntax works on objects and means something else entirely:

{ ...a, ...b }

For arrays, spread appends. For objects, it overwrites by key, with later sources winning:

{ ...{ x: 1, y: 2 }, ...{ y: 9 } };   // { x: 1, y: 9 }

Two things to remember. The merge is shallow, so a nested object in b replaces the one in a outright rather than combining with it, which is the subject of shallow versus deep copying. And Object.assign(target, a, b) does the same thing while mutating target, in the same way push mutates an array.

The parallel is worth holding onto: spread on an array concatenates, spread on an object overwrites. Reaching for one when you meant the other produces a result that looks nearly right.

Which approach costs what

For ordinary array sizes none of this matters, and the differences show up on large data.

concat and spread both build a new array, so merging two arrays of a million items allocates two million slots. Repeatedly merging in a loop is the version to avoid:

// allocates a new array on every iteration
let all = [];
for (const chunk of chunks) all = [...all, ...chunk];

// one array, appended to
const all = [];
for (const chunk of chunks) for (const item of chunk) all.push(item);

The first grows quadratically with the number of chunks, because each iteration copies everything accumulated so far. On a handful of chunks it is fine and clearer; on hundreds it is the reason a page hangs.

flat() does the same job in one call and is both shorter and faster:

const all = chunks.flat();

That is the answer whenever you have an array of arrays and want one array, and it avoids both the loop and the quadratic version.

Merging in place versus building new

Worth stating plainly, because it is the decision underneath every option here.

Building a new array (spread, concat, flat) is the safe default. Nothing else holding a reference to the originals is affected, which matters when the arrays came from props, state, a cache, or a function argument.

Merging in place (push) is right when you own the array and are accumulating into it, and it avoids the repeated allocation above.

The failure mode is mutating something you did not own. A function that takes an array and pushes into it changes the caller's data, which is the same class of surprise as sort reordering its input. If a function takes an array and returns one, it should almost always leave the argument alone.

Inserting rather than appending

Merging at a position is the same tools with a slice:

[...a.slice(0, i), ...b, ...a.slice(i)];

That splits a at index i and puts b between the halves, returning a new array. The mutating equivalent is splice, which takes a start, a delete count, and the items to insert:

a.splice(i, 0, ...b);

The 0 means "delete nothing", which is the argument people forget, and the same spread-as-arguments limit from above applies to b.

Worth knowing that splice returns the removed items, not the resulting array, so assigning its result is the same mistake as assigning push. toSpliced is the non-mutating version and returns the new array, which is usually what you wanted.

Three common mistakes

Using push(...arr) on a large array. It throws a RangeError past a few hundred thousand items, and only on real data.

Expecting spread to flatten. It goes one level. Nested arrays stay nested; use flat().

Deduplicating objects with a Set. Two objects with the same contents are different values, so nothing is removed. Key on a field with a Map.

Merging results from several requests

The most common real merge is combining responses, and it has a decision most people skip.

const [users, admins] = await Promise.all([fetchUsers(), fetchAdmins()]);
const everyone = [...users, ...admins];

That works when the two lists are disjoint. When they overlap, which they usually do, you get duplicates of anyone who is both, and the Map approach from above is what you want.

The related question is what to do when one request fails. Promise.all rejects if any of them does, so you get nothing:

const results = await Promise.allSettled([fetchUsers(), fetchAdmins()]);
const everyone = results
  .filter((r) => r.status === "fulfilled")
  .flatMap((r) => r.value);

allSettled waits for all of them and reports each outcome separately, so a partial result is available. flatMap then merges the successful arrays in one step, which is neater than spreading them individually when the number of sources varies.

Which you want depends on whether a partial list is useful or misleading. A dashboard can show what loaded; a checkout total cannot be computed from some of the line items.

Preserving order

Neither spread nor concat sorts anything, so the result is the first array's items followed by the second's. That is usually fine and occasionally not:

[...a, ...b].sort((x, y) => x.date - y.date);

Merging two already-sorted lists and re-sorting the whole thing does more work than necessary, though on realistic sizes it is not worth optimising. The point is to remember that merged order is concatenation order, not anything smarter, and to sort explicitly when the combined list needs an order of its own.

Quick reference

[...a, ...b]                     // merge, new array
a.concat(b)                      // same
a.push(...b)                     // in place, breaks on large arrays
[...new Set([...a, ...b])]       // merge and dedupe primitives
[1, [2, [3]]].flat(Infinity)     // flatten fully
a.flatMap((x, i) => [x, b[i]])   // interleave

Want to break the stack yourself and see where it happens? Start with the JavaScript track.

Keep reading

More from the blog

The same object iterated with Object.keys and with for-in, where for-in returns an extra inherited key
August 31, 2026·9 min readLearn

How to loop through an object in JavaScript

To loop through an object in JavaScript, for...in walks inherited properties too. And integer-like keys come out first, whatever order you wrote.

Read more
A spread copy sharing its nested object with the original beside a deep clone with its own separate nested object
August 29, 2026·8 min readLearn

Shallow vs deep copy in JavaScript: how to clone properly

To copy an object in JavaScript, spread copies the top level only, so nested objects stay shared. Here is what each method keeps and destroys.

Read more
{ }
✦

Ready to write some code?

Put this into practice - start your first free lesson. No setup, no credit card.

Start learning free
Devpuff

Learn to code by doing. One tiny, playful lesson at a time.

Learn
ProgramsCoursesPricing
Company
AboutBlogResourcesAffiliates
Support
Help CenterContactStatus
Programs
Frontend DevelopmentFull-Stack DevelopmentPython Developer
Courses
Advanced ReactAsync JavaScriptAsync PythonCSS BasicsCSS LayoutDSA Basics
Learn to Code
Learn JavaScriptLearn PythonLearn SQLLearn HTMLLearn CSSLearn ReactBrowse All Topics
Platform Comparisons
Devpuff vs CodecademyDevpuff vs MimoDevpuff vs Sololearn
© 2026 Devpuff. All rights reserved.Privacy PolicyTerms and ConditionsCookies PolicyRefund Policy
Devpuff vs freeCodeCamp
Read All Comparisons