How do I properly remove an element from a JavaScript array?

I’m working on a small JavaScript project and need to remove specific elements from an array by value or index, but I’m getting confused between methods like splice, filter, and delete. Some attempts are leaving empty slots or not updating the original array as I expect. Can someone explain the right ways to remove items from an array, with examples of when to use each method for best performance and clean results?

You are mixing three different behaviors. Quick breakdown with examples.

  1. Remove by index, mutate original array

Use splice.

const arr = [10, 20, 30, 40]

// remove 1 element at index 1
arr.splice(1, 1) // returns [20]
console.log(arr) // [10, 30, 40]

// remove 2 elements starting at index 0
arr.splice(0, 2)

Rule: first arg is start index, second arg is how many to remove.

Do NOT use delete for array holes. Example:

const a = [1, 2, 3]
delete a[1]
console.log(a) // [1, empty, 3]
console.log(a.length) // 3

You get a sparse array, length stays the same, loops act weird.

  1. Remove by value, keep original reference

Find the index, then splice.

const arr = [‘a’, ‘b’, ‘c’, ‘b’]

const idx = arr.indexOf(‘b’)
if (idx !== -1) {
arr.splice(idx, 1)
}

Now arr is [‘a’, ‘c’, ‘b’].

If you want to remove all matches, use a loop.

let idx
while ((idx = arr.indexOf(‘b’)) !== -1) {
arr.splice(idx, 1)
}

  1. Remove by value, return new array, do not mutate original

Use filter.

const arr = [1, 2, 3, 2]
const withoutTwos = arr.filter(x => x !== 2)

console.log(arr) // [1, 2, 3, 2]
console.log(withoutTwos) // [1, 3]

Good if you use functional style or React state.

  1. Remove by index, return new array

Use slice and spread.

const arr = [10, 20, 30, 40]
const indexToRemove = 1

const newArr = [
…arr.slice(0, indexToRemove),
…arr.slice(indexToRemove + 1)
]

arr stays the same.

  1. Summary rules

Use splice when you want to change the original array by index.

Use filter when you want a new array by value.

Avoid delete on arrays. It leaves holes and keeps the length.

You’re not crazy, JavaScript arrays are just petty like that sometimes.

@kakeru already covered the “classic trio” (splice, filter, delete) really well, so I’ll avoid re-explaining those line by line. Instead, here are a few extra angles that usually trip people up and how to avoid the empty-slot / weird-behavior mess.


1. The “delete” trap is worse than it looks

You already saw that delete arr[i] leaves a hole. The subtle extra problem: some array methods actually skip those holes.

const a = [1, 2, 3];
delete a[1];

a.forEach(v => console.log(v));   // logs 1 and 3, but not 2
for (const i in a) console.log(i) // '0', '2'
console.log(a.length);            // 3

So your logic might think “index 1 is there but undefined” while array methods treat it like it doesn’t exist. That’s how bugs get born. If you want undefined as a valid value, assign it:

a[1] = undefined;  // no hole, length & indices stay sane

Still not “removing” anything, but at least it’s consistent.


2. Remove by value with conditions, not just equality

Instead of:

// remove all 'b'
arr = arr.filter(x => x !== 'b');

You can use any condition:

// keep only active users
const activeUsers = users.filter(u => u.active);

// remove items under some threshold
const cleaned = nums.filter(n => n >= 0);

This is way nicer than hunting indices if your rule is “anything that matches this logic,” not just a single value.


3. Removing the first or last element

If your “index to remove” is specifically at the start or end, you can simplify:

const arr = [1, 2, 3, 4];

arr.shift();  // remove first -> [2, 3, 4]
arr.pop();    // remove last  -> [1, 2, 3]

Both mutate the array, similar to splice, but shorter and clearer for those specific positions.

I slightly disagree with pretending splice is always the best for mutation: for “remove first” and “remove last,” shift and pop are just more readable.


4. Removing one item by value without re-scanning

A pattern I like:

function removeFirstMatch(arr, predicate) {
  const idx = arr.findIndex(predicate);
  if (idx >= 0) arr.splice(idx, 1);
  return arr;
}

// remove first user with id = 5
removeFirstMatch(users, u => u.id === 5);

Better than using indexOf when your “value” is actually a property on an object.


5. Immutable update for specific index (like in React)

Instead of:

// remove at index i, keep original
const newArr = arr.filter((_, idx) => idx !== i);

This avoids splice and is easier to read than slicing + spreading for most cases:

const i = 2;
const newArr = arr.filter((_, idx) => idx !== i);

Yes, it loops the whole array, but for most UI / state use cases it’s totally fine and very clear.


6. If you’re seeing “empty slots”, double check:

  1. That you did not use delete.
  2. That some library / framework is not doing arr.length = ... on you.
  3. That your logs aren’t just showing empty cells from earlier accidental deletes.

Quick sanity check:

console.log(arr, arr.length);
console.log(Object.keys(arr));  // shows actual existing indices

If length is bigger than the highest index in Object.keys, you’ve got holes.


TL;DR:

  • Never use delete to remove from arrays unless you want a sparse array (you probably don’t).
  • For mutating: splice, pop, shift.
  • For new array: filter (by value or condition) or filter-by-index for a single removal.
  • If you see “empty” in dev tools, you’ve hit sparse territory and should refactor the removal method.

You are basically choosing between three “schools” of array removal:

  1. Index-based and mutating (splice, pop, shift)
  2. Value/condition-based and immutable (filter, slice + spread)
  3. “Pretend it’s gone” hacks (delete, setting undefined)

@codecrafter and @kakeru covered the first two really well, so here are some extra patterns and gotchas that usually show up once you move beyond trivial examples.


1. “Remove, but keep order stable and logic clear”

For a single index in an immutable style, I actually prefer this over the slice + spread sandwich:

function removeIndex(arr, index) {
  return arr.filter((_, i) => i !== index);
}

Pros:

  • Easy to read, especially for beginners or in React state.
  • No mental math with slice(0, i) and slice(i + 1).

Con:

  • Iterates the whole array, so for very large arrays in performance-critical code, splice may be better.

I slightly disagree with the idea that slice+spread is more “standard”. In many codebases, a tiny helper like removeIndex is clearer and gets reused.


2. “Remove first match by predicate” pattern

For objects where you care about some property:

function removeFirstBy(arr, predicate) {
  const idx = arr.findIndex(predicate);
  if (idx < 0) return arr;
  const copy = arr.slice();
  copy.splice(idx, 1);
  return copy;
}

// Example: remove first user with id 42
const newUsers = removeFirstBy(users, u => u.id === 42);

This gives you:

  • Non-mutating behavior.
  • No repeated indexOf or full array rescans.
  • Works with complex matching logic, not just equality.

3. “Remove many by predicate, but mutate”

If you actually want to mutate in-place and remove multiple items, a backward loop is safer than repeatedly calling splice in a forward loop:

function removeAllInPlace(arr, predicate) {
  for (let i = arr.length - 1; i >= 0; i--) {
    if (predicate(arr[i], i, arr)) {
      arr.splice(i, 1);
    }
  }
  return arr;
}

// Example: remove all negatives, mutating the same array
removeAllInPlace(nums, n => n < 0);

Why backward?

  • When you splice forward, indices shift and you can accidentally skip elements.

4. When delete is actually acceptable

Both @codecrafter and @kakeru say “avoid delete,” and that is good default advice. I will add a tiny nuance: delete is sometimes fine if you genuinely want a sparse array, for example using an array as a loose sparse map and you do not rely on index iteration.

Example where it is intentional:

const cache = [];
cache[1000] = 'value';
delete cache[1000]; // remove that sparse entry

Cons:

  • Methods like forEach, map, filter skip holes.
  • Debugging gets confusing when you see empty in devtools.

So for normal application data, still avoid it.


5. “Logical delete” instead of structural delete

Sometimes the best option is: do not remove at all, just mark as removed:

item.deleted = true;

// use it:
const visibleItems = items.filter(i => !i.deleted);

This is useful when:

  • You want undo/redo.
  • You need to keep history or indices stable.
  • You do not want React keys or UI positions to reshuffle.

6. Quick mental cheat sheet

  • Need to change original array by index: splice, pop, shift.
  • Need a new array based on a condition: filter.
  • Need a new array minus specific index: filter((_, i) => i !== index) or slice+spread.
  • Seeing empty in your array inspector: at some point you used delete or manually manipulated length.

Both @codecrafter and @kakeru gave solid baselines; the patterns above should keep you from running into the “empty slot” / “why did this loop skip my element” headaches again.