To insert an item into a JavaScript array at a specific index, you have two real choices in 2026: splice if you want to mutate the array in place, or toSpliced if you want a new array and the original left untouched.
const months = ["Jan", "Mar", "Apr"];
// Mutates `months`, returns the removed elements ([] here):
months.splice(1, 0, "Feb");
// months is now ["Jan", "Feb", "Mar", "Apr"]
// Copies: returns a new array, `months` is unchanged:
const updated = months.toSpliced(1, 0, "Feb");That second argument, the 0, is the part people forget. splice(start, deleteCount, ...items) is a delete-and-insert in one call; passing 0 for deleteCount means "delete nothing, just insert here." Both methods share that exact signature, so everything below applies to either one.
splice: mutating, in place
splice(start, 0, item) inserts item before the element currently at start, shifts the rest up, and modifies the array you called it on. Its return value is the array of removed elements, not the new array, which trips people up:
const arr = [1, 2, 4];
const result = arr.splice(2, 0, 3);
// arr === [1, 2, 3, 4] (the insert)
// result === [] (nothing was removed)So const x = arr.splice(...) does not give you the spliced array. The spliced array is arr, because splice changed it underneath you. If you wrote const updated = arr.splice(2, 0, 3) expecting updated to be the new array, that is a bug: updated is [], and arr was silently mutated. This is the single most common splice mistake.
toSpliced: copying, leaves the original alone
toSpliced (ES2023) is the copying counterpart. Same arguments, same insert behavior, but it returns the new array and never touches the source:
const arr = [1, 2, 4];
const updated = arr.toSpliced(2, 0, 3);
// updated === [1, 2, 3, 4]
// arr === [1, 2, 4] (untouched)This is what you want in React state updates, Redux reducers, or anywhere you are working with values you should not mutate. Before toSpliced shipped, the workaround was to clone first and then splice the clone (const copy = [...arr]; copy.splice(...)), which is two steps and easy to get wrong. Now it is one call.
toSpliced is part of the immutable array-method family (toReversed, toSorted, with, toSpliced), Baseline since 2023: Chrome 110, Firefox 115, Safari 16, and Node 20+. If you target environments older than that, either keep the clone-then-splice pattern or pull in a polyfill.
The negative-index and delete-count semantics (both methods)
The two methods behave identically here, which is convenient.
A negative start counts back from the end. splice(-1, 0, x) inserts before the last element:
const arr = ["a", "b", "d"];
arr.splice(-1, 0, "c"); // ["a", "b", "c", "d"]A start past the end of the array clamps to the length, so the item lands on the end (same as push). And because the second argument is deleteCount, you can replace instead of insert by passing a non-zero count: arr.splice(1, 1, "x") deletes one element at index 1 and inserts "x" in its place. A deleteCount of 0 or negative deletes nothing, which is exactly the insert case.
Insert via spread (a copy, no method call)
If you like the spread style, you can build the new array directly with two slice calls around the insertion point:
function teInsertAt(arr, index, item) {
return [...arr.slice(0, index), item, ...arr.slice(index)];
}
teInsertAt([1, 2, 4], 2, 3); // [1, 2, 3, 4]This is non-mutating (like toSpliced) and works in any environment that has spread, so it is the natural fallback when you want copy semantics but cannot rely on Node 20. It reads clearly, though toSpliced is shorter and handles the negative-index math for you. For inserting at the very ends, just reach for [item, ...arr] (prepend) or [...arr, item] (append).
Do not extend Array.prototype
Older tutorials (this one originally followed that style) suggest adding an Array.prototype.insert method so you can call arr.insert(2, item). Do not. Monkey-patching built-in prototypes is genuinely risky:
- Name collisions break the platform. A property you add can clash with a future native method of the same name, and your version wins, breaking real code. This is not hypothetical: the array grouping proposal tried to land on the prototype first as
Array.prototype.groupBy, then asArray.prototype.group, and TC39 had to abandon the prototype entirely and ship it as the staticObject.groupBy/Map.groupByinstead. ThegroupByname collided with an incompatibleArray.prototype.groupBythat the Sugar library had monkey-patched onto live sites;groupthen broke code that used arrays as ad-hoc hashmaps. The standard had to move out of the way of the pollution twice. See the modern array methods reference for that whole saga and the corrected API. - It is enumerable and global. A prototype method leaks into every array in the program, including arrays from libraries you did not write, and a careless
for...inover an array will now iterate your method.
Keep the helper as a plain function (the teInsertAt above), or just use toSpliced directly. A free function you import is testable, scoped, and never collides with the platform.
FAQ
See also
- Modern JavaScript array methods:
at,flat,toReversed, the copying methods, and theArray.prototype.grouprename that becameObject.groupBy. - How to merge two arrays in JavaScript:
concatvs spread vspush, and the memory trade-off for large arrays.
Sources
Authoritative references this article was fact-checked against.
- Array.prototype.splice() (MDN)developer.mozilla.org
- Array.prototype.toSpliced() (MDN)developer.mozilla.org
- Array Grouping proposal — why it became Object.groupBy / Map.groupBy (TC39)github.com





