Remove an Element From a Swift Array (and Fix the ‘let’ Error)

Remove an Element From a Swift Array (and Fix the 'let' Error)

Written by

in

Quick answer: Declare the array with var instead of let, then call animals.remove(at: 2) to delete by index or animals.removeAll { $0 == "chimps" } to delete by value. If the array must stay a constant, use filter to build a new array without the element instead.

Why this happens

Array is a value type, and its removal methods (remove(at:), removeFirst(), removeAll(where:)) are declared mutating because they change the array’s storage in place. A let declaration promises the value never changes, so the compiler refuses the call with:

Cannot use mutating member on immutable value: 'animals' is a 'let' constant

The second common failure is an index that no longer matches the array. remove(at:) traps at runtime with Fatal error: Index out of range if the index is negative or greater than or equal to count. This usually happens when you capture an index, remove something else, then reuse the stale index — every element after the removed one shifts down by one.

How to fix it

1. Change let to var and remove by index

This is the direct fix for the compiler error. remove(at:) deletes the element at that position and returns it, so the result is discardable if you don’t need it.

var animals = ["cats", "dogs", "chimps", "moose"]

animals.remove(at: 2)
// animals is now ["cats", "dogs", "moose"]

If you only need the array to be constant for a while, keep the mutation local: copy into a var, modify, then assign back to your let property.

2. Remove by value when you don’t know the index

remove(at:) needs a position, so look the position up first. firstIndex(of:) requires the element type to be Equatable, which String, Int and most model types are.

var animals = ["cats", "dogs", "chimps", "moose"]

if let index = animals.firstIndex(of: "chimps") {
    animals.remove(at: index)
}
// ["cats", "dogs", "moose"]

The if let matters: when the value isn’t present, firstIndex(of:) returns nil and there is nothing to remove. For a custom type, either conform to Equatable or use the predicate version:

if let index = animals.firstIndex(where: { $0.hasPrefix("chi") }) {
    animals.remove(at: index)
}

3. Remove every matching element

removeAll(where:) mutates in place and removes all elements that satisfy the predicate — no index bookkeeping, and no risk of skipping elements the way a for loop with remove(at:) would.

var animals = ["cats", "dogs", "chimps", "moose", "chimps"]

animals.removeAll { $0 == "chimps" }
// ["cats", "dogs", "moose"]

There is also a shorthand when you just want to empty the array: animals.removeAll(), or animals.removeAll(keepingCapacity: true) if you plan to refill it and want to skip a reallocation.

4. Remove the first or last element safely

removeFirst() and removeLast() are the fast paths, but both trap on an empty array.

var animals = ["cats", "dogs", "chimps", "moose"]

animals.removeFirst()   // "cats"
animals.removeLast()    // "moose"
// ["dogs", "chimps"]

Guard the empty case, or use popLast(), which returns an optional instead of crashing:

if !animals.isEmpty {
    animals.removeFirst()
}

if let last = animals.popLast() {
    print("removed \(last)")
}

5. Keep the original array unchanged

When the array is a let, or when other code still needs the full list, build a new array instead of mutating. filter is the cleanest option and works on constants:

let animals = ["cats", "dogs", "chimps", "moose"]
let pets = animals.filter { $0 != "chimps" }
// ["cats", "dogs", "moose"]

dropFirst() and dropLast() do the same job for the ends of the array, but they return an ArraySlice, not an Array. Wrap the result if you need a real array:

let rest = Array(animals.dropFirst())   // ["dogs", "chimps", "moose"]

That distinction bites people who keep the slice: an ArraySlice keeps the original indices, so rest[0] on the example above is a crash. Always convert with Array(...) unless you are passing the slice straight into another algorithm.

6. Remove several elements without index drift

Deleting by index inside a loop is the classic source of Index out of range. Either iterate the indices in reverse, or rebuild the array in one pass:

var animals = ["cats", "dogs", "chimps", "moose"]
let doomed: Set<Int> = [0, 2]

animals = animals.enumerated()
    .filter { !doomed.contains($0.offset) }
    .map(\.element)
// ["dogs", "moose"]

For a contiguous block, removeSubrange(_:) is shorter:

var animals = ["cats", "dogs", "chimps", "moose"]
animals.removeSubrange(1...2)
// ["cats", "moose"]

One performance note: remove(at:) is O(n) because everything after the index shifts down. If order doesn’t matter and you are deleting many items from a large array, swap the target with the last element and drop the tail — that is O(1) per removal:

animals.swapAt(1, animals.count - 1)
animals.removeLast()

How to prevent it

  • Declare arrays you intend to mutate with var; use let only for arrays that are built once and never changed.
  • Never hardcode an index you haven’t bounds-checked — prefer firstIndex(of:) or firstIndex(where:) and unwrap the result.
  • Don’t call remove(at:) inside a for loop over the same array; use removeAll(where:), filter, or reversed indices.
  • Remember removeFirst(), removeLast() and remove(at:) trap on bad input; popLast() and firstIndex return optionals instead.
  • Convert ArraySlice results from dropFirst()/dropLast() back to Array before indexing them.
  • Use Set lookups instead of repeated firstIndex(of:) calls when removing many values from a large array.

FAQ

Why does animals.remove(at: 2) fail with “Cannot use mutating member on immutable value”?

Because animals was declared with let. remove(at:) is a mutating method, and Swift won’t let a mutating method run on a constant. Change the declaration to var, or switch to filter if the array genuinely has to stay constant.

Does remove(at:) return the removed element?

Yes. animals.remove(at: 2) returns "chimps" and is marked discardable, so you can ignore the result or capture it. The same is true for removeFirst() and removeLast().

What is the difference between removeAll(where:) and filter?

removeAll(where:) mutates the existing array in place and requires a var. filter leaves the original untouched and returns a brand-new array, so it works on let constants and on arrays you are passing around elsewhere.

Why does dropFirst() give me an ArraySlice instead of an Array?

dropFirst() and dropLast() are slice-producing operations that avoid copying. The slice shares indices with the original array, so wrap it in Array(...) when you need zero-based indexing or a value you can store independently.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.