Swift forEach: Prefer forEach Only for Side Effects
Swift's forEach method is a convenient way to run the same closure for every element in a collection. It is useful when you want to perform an action for its side effects, but it is usually the wrong choice when you need control flow, early exit, or a transformed result.
Quick answer: Use forEach when you want to do something for each element, such as printing, logging, or updating external state. Prefer a for-in loop when you need break, continue, or clearer iteration.
Difficulty: Beginner
You'll understand this better if you know: basic Swift collections, closures, and the difference between returning a value and performing an action.
1. What Is forEach?
forEach is a method on Swift sequences and collections that calls a closure once for every element. The closure receives each element in order, and the method itself returns Void.
- It iterates through items one at a time.
- It is designed for actions, not results.
- It does not support loop control statements like break or continue.
- It is often less flexible than a for-in loop.
That last point is why the rule of thumb matters: if you are using forEach to build a new collection or to search for a value, there is usually a better tool.
2. Why forEach Matters
forEach can make code read like a list of actions applied to each element. That can be nice when the intent is simple, such as printing, writing to a file, or notifying another object.
However, many beginners reach for it when they really want a loop with control flow. In those cases, forEach can make code harder to understand because it hides the fact that you are not returning a result and you cannot stop early in the usual way.
Prefering forEach only for side effects helps keep code readable and prevents subtle bugs where a developer expects loop behavior that forEach does not provide.
3. Basic Syntax or Core Idea
The minimal form of forEach uses a closure that receives each element:
numbers.forEach { number in
print(number)
}This means: take every value in numbers, bind it to number, and run the closure body.
How the closure works
The closure you pass to forEach can use the element, update outside state, or call other functions. Because forEach ignores any return value from the closure, it is not a good fit for producing a new array.
What the method does not do
forEach does not change the collection by itself. It also does not let you leave the loop early with break. If you need those behaviors, use for-in instead.
4. Step-by-Step Examples
Example 1: Printing values
A straightforward side effect is printing each element. This is one of the clearest uses of forEach.
let names = ["Ava", "Noah", "Mia"]
names.forEach { name in
print("Hello, \(name)")
}This code performs an action for each name. It does not create a new array or change the original one.
Example 2: Logging items conditionally
You can use forEach when the main purpose is to send each item to a logger or debugging output.
let tasks = ["download", "parse", "save"]
tasks.forEach { task in
if task.hasPrefix("s") {
print("Important task: \(task)")
}
}The closure still runs for every item, but only some items trigger the side effect.
Example 3: Updating external state
Sometimes a closure updates a value outside itself, such as a running total. This is a side effect, so forEach can be reasonable.
let prices = [12, 8, 5]
var total = 0
prices.forEach { price in
total += price
}
print(total)This works because the goal is to update total, not to create a transformed collection.
Example 4: Comparing with a for-in loop
Here is the same iteration written as a for-in loop. Notice how the loop gives you more control.
for price in prices {
if price < 10 {
continue
}
print(price)
}If you need to skip values or stop early, the for-in loop is clearer and more capable.
5. Practical Use Cases
- Printing debug information for every element in a collection.
- Sending each item to a logger, analytics event, or notification service.
- Applying external updates such as accumulating totals or building a report string.
- Triggering one-off side effects where the result is stored elsewhere.
- Running cleanup or bookkeeping logic over a small collection.
These are all cases where the important outcome is the action, not a returned collection.
6. Common Mistakes
Mistake 1: Using forEach to build a new array
Many beginners try to use forEach when they want to transform values. That often leads to extra mutable state and less readable code.
Problem: The code below uses forEach to append transformed values into another array. It works, but it is harder to read than a transformation method designed for that job.
let numbers = [1, 2, 3]
var doubles: [Int] = []
numbers.forEach { number in
doubles.append(number * 2)
}Fix: Use map when the goal is to create a new array from each element.
let numbers = [1, 2, 3]
let doubles = numbers.map { number in
number * 2
}The corrected version is better because it expresses transformation directly instead of relying on side effects.
Mistake 2: Expecting break to stop forEach
forEach is not a full loop statement, so it does not support break in the way a for-in loop does.
Problem: Trying to use break inside forEach causes a compile-time error because the closure is not a loop body.
let scores = [10, 22, 31]
scores.forEach { score in
if score > 20 {
break
}
print(score)
}Fix: Use for-in when you need to stop early.
let scores = [10, 22, 31]
for score in scores {
if score > 20 {
break
}
print(score)
}The for-in version works because break is part of the language's loop syntax.
Mistake 3: Using return to mean “return from the loop”
Inside forEach, return exits only the closure for the current element. It does not stop the whole iteration.
Problem: This code looks like it should stop when it finds an even number, but it keeps processing later elements because return only leaves the closure body for that one item.
let values = [1, 3, 4, 6]
values.forEach { value in
if value % 2 == 0 {
return
}
print(value)
}Fix: Use a loop with break, or use a searching method if your real goal is to find an element.
for value in values {
if value % 2 == 0 {
break
}
print(value)
}The corrected version matches the intended control flow, so later elements are not processed once the condition is met.
7. Best Practices
Practice 1: Use forEach when the result is intentionally discarded
If the output of the iteration is not needed, forEach can communicate that the purpose is the side effect itself.
let ids = ["A12", "B45", "C98"]
ids.forEach { id in
print("Processing \(id)")
}This is a good fit because the code is about doing something to each item, not producing a transformed value.
Practice 2: Prefer for-in when you need control flow
When the iteration needs to stop, skip values, or nest more complex logic, a for-in loop is usually clearer.
for id in ids {
if id == "B45" {
break
}
print(id)
}This version is better because it makes the control flow explicit.
Practice 3: Avoid side effects when a return value would be better
If you are computing a new collection, count, or derived value, use the collection method that returns that result instead of writing to external mutable variables.
let amounts = [5, 10, 15]
let filtered = amounts.filter { amount in
amount > 8
}This keeps the data flow easier to test and reason about than mutating an external array inside forEach.
8. Limitations and Edge Cases
- forEach cannot use break or continue to control iteration.
- return inside the closure returns from that closure call only, not from the containing function.
- It is usually a poor choice when you need to build a new array or search for a matching value.
- On lazy sequences, forEach still evaluates items one by one, but it does not change the fact that the result is Void.
- Using forEach for large or complex workflows can hide the actual intent of the code.
These limitations are not bugs; they are part of the method's design. The method is intentionally simple.
9. Practical Mini Project
Let's build a tiny command-line report that prints each product name and calculates the total inventory value as a side effect.
struct Product {
let name: String
let price: Int
let quantity: Int
}
let inventory = [
Product(name: "Notebook", price: 3, quantity: 10),
Product(name: "Pen", price: 2, quantity: 25),
Product(name: "Folder", price: 4, quantity: 8)
]
var totalValue = 0
inventory.forEach { product in
let lineValue = product.price * product.quantity
totalValue += lineValue
print("\(product.name): $\(lineValue)")
}
print("Total inventory value: $\(totalValue)")This example is appropriate for forEach because the side effects are the real output: printing each line and updating a total.
10. Key Points
- forEach is best when the purpose is an action, not a returned value.
- It is a method on sequences and collections that runs a closure for each element.
- It does not support break or continue.
- Use map for transformations and for-in for flexible loops.
- If your code depends on side effects, make that intent obvious.
11. Practice Exercise
- Create an array of five city names.
- Use forEach to print each city in uppercase.
- Keep a separate counter for how many city names contain the letter a.
- Do not build a new array.
Expected output: Five uppercase city names printed, followed by the count of cities containing a.
Hint: Use print and a mutable counter, but do not try to return values from forEach.
let cities = ["Paris", "Tokyo", "Lima", "Oslo", "Cairo"]
var countWithA = 0
cities.forEach { city in
let uppercased = city.uppercased()
print(uppercased)
if city.lowercased().contains("a") {
countWithA += 1
}
}
print("Cities containing a: \(countWithA)")12. Final Summary
forEach is a clear and useful tool when you want to perform an action for each element in a collection. Its strength is simplicity: it reads well when the code is doing side effects such as printing, logging, or updating external state.
Its weakness is control flow. If you need to stop early, skip values, or produce a transformed result, a for-in loop, map, or another collection method is usually a better fit. Keeping that distinction in mind will make your Swift code easier to read and less error-prone.
If you want to continue learning, compare forEach with map, filter, and for-in next, since those are the methods and statements most often confused with it.