JavaRush /Courses /Kotlin SELF /forEach and side effects: when it makes sense

forEach and side effects: when it makes sense

Kotlin SELF
Level 22, Lesson 0
Available

1. What is forEach, and how is it different from for?

When you see forEach, the first reaction is usually: “Aha, it’s like a for loop, just fancier.” And yes, it does iterate over elements. But its meaning is a bit different: forEach is a function call for each element, not a separate language construct. Because of that, the nuances change: what you can/can’t do inside, how the code reads, and why forEach is most often used where you need to perform an action, not “compute something and return it.”

If we compare very roughly, for is “I control the iteration process,” while forEach is “the collection iterates itself, and I give it an instruction for what to do with each element.”

It’s useful to keep this table in mind (it’s not about “right/wrong,” it’s about intent):

Tool What it is in Kotlin Typical goal Return value
for (...) { ... }
language construct iteration + control (break/continue) none (it’s control flow)
forEach { ... }
higher-order function “do an action for each element”
Unit

Kotlin coding conventions explicitly mention that when choosing between a regular loop and forEach, it’s often worth preferring for, except for cases like a nullable receiver or when forEach is part of a call chain.

2. The forEach signature and the role of Unit

When you start using collection functions, your brain has a sneaky habit: “If it’s a function, it must return something, and I can store it in a variable.” With forEach, almost always no. forEach is meant to run code on each element, and its “result” is a side effect (for example, printing to the console). That’s why forEach returns Unit, i.e., “nothing useful as a value.”

Let’s look at a mini-example:

fun main() {
    val names = listOf("Ann", "Bob", "Chris")

    names.forEach { name ->
        println("Hello, $name") // Hello, Ann ... and so on
    }
}

Here everything is logical: we want to print a greeting for each name. That is exactly the purpose of forEach.

And here’s what beginners often try to do:

fun main() {
    val xs = listOf(1, 2, 3)

    val result = xs.forEach { x ->
        println(x)
    }

    println(result) // this will be kotlin.Unit
}

result here is Unit. That is, “we stored in a variable the fact that we didn’t return anything.” If you’ve ever collected candy wrappers—congratulations, you’re already ready for this style.

3. forEach for actions: printing, logging, calling a function

forEach is especially good when you already have your data and you want to “apply” it to the world: display it, write to a log, send it somewhere, call a function that performs a clear action. The most honest sign that forEach is appropriate: inside the lambda you do something “outward,” not build a new data structure.

Printing is the most honest side effect

fun main() {
    val xs = listOf(10, 20, 30)
    xs.forEach { x ->
        println("x = $x") // x = 10, then 20, then 30
    }
}

Calling a function “with an effect”

Suppose we have a function that prints a warning (this is an action, not a computation):

fun warn(message: String) {
    println("WARNING: $message")
}

fun main() {
    val problems = listOf("Empty input", "Wrong format", "Out of range")
    problems.forEach { p ->
        warn(p) // WARNING: Empty input ...
    }
}

A small diagram: where the meaning of forEach lives

Sometimes it helps to picture it like this:

flowchart LR
    A[Data collection] --> B["forEach { ... }"]
    B --> C[ACTION: println / log / function call]

This isn’t “math,” it’s an “executor”: data → processed → action performed.

4. Useful techniques with forEach

Callable reference ::println: when “shorter” really is better

After lambdas, you learned callable references: :: lets you pass a ready-made function where a lambda is expected. This is especially nice when the lambda would be “just call one function with one argument.”

For example, instead of:

fun main() {
    val xs = listOf(1, 2, 3)
    xs.forEach { x -> println(x) } // 1 2 3
}

you can do:

fun main() {
    val xs = listOf(1, 2, 3)
    xs.forEach(::println) // 1 2 3
}

This reads in a “human” way: “for each element — println.”

There’s an important style point here: if a lambda becomes complex, it’s better to name the parameter explicitly rather than use it. Kotlin conventions also call this out: short, unconfigured lambdas use it; nested/complex ones are better with explicit parameters.

?.forEach: a convenient pattern for nullable collections

There’s one place where forEach really beats a normal for. It’s when the collection can be null. With for, you’d have to write a check:

fun main() {
    val maybeItems: List<String>? = listOf("a", "b")

    if (maybeItems != null) {
        for (x in maybeItems) {
            println(x)
        }
    }
}

But with forEach you get a direct and nice pattern:

fun main() {
    val maybeItems: List<String>? = listOf("a", "b")
    maybeItems?.forEach { println(it) } // a, then b
}

Here it reads literally like an English sentence: “if the list exists, do an action for each element.”

And this is exactly the case that’s often considered justified for forEach: when ?.forEach removes an extra if and makes the code shorter without turning it into a riddle.

5. Side effects: what they are and why you should use them carefully

The phrase “side effect” sounds like someone is about to talk about medicine. But in programming it’s simpler: a side effect is changing something outside the return value. Printing to the console is a side effect. Writing to a file is a side effect. Changing an external variable is also a side effect.

forEach is most often used exactly where side effects are appropriate. It’s just important to understand: side effects make code less predictable if you overuse them. Roughly: if a function returns a number, you can verify its result. If a function “changed something somewhere,” it’s harder to verify.

“Accumulating a sum” — possible, but do it intentionally

This example is in the “you can do this, but remember what you’re doing” category:

fun main() {
    val xs = listOf(10, 20, 30)

    var sum = 0
    xs.forEach { x ->
        sum += x // modifying an external variable (side effect)
    }

    println("sum = $sum") // sum = 60
}

Why is this considered a side effect? Because sum is outside forEach, and we’re changing it.

Note that I’m not saying “you must not do this.” I’m saying: “you can, but you should understand that you’ve entered mutable-state mode.”

“Building a string” — also a side effect, but often practical

Since you already know StringBuilder, here’s a neat example: we don’t print at each step; we build the text and print it once:

fun main() {
    val names = listOf("Ann", "Bob", "Chris")
    val sb = StringBuilder()

    names.forEach { name ->
        sb.appendLine("Hello, $name")
    }

    print(sb.toString())
    // Hello, Ann
    // Hello, Bob
    // Hello, Chris
}

Technically, this is also a side effect (we mutate sb), but it’s “local” and clear: the builder is created nearby, used right here, doesn’t leak outward, and that keeps the code readable.

6. forEach in a CLI project: printing an expense list

From this day on, we start writing more “collection-oriented” Kotlin code, and our practical CLI project (a notional expense tracker) is a great place to apply forEach. In the project we already have a collection of expenses, and the first practical task is to output them nicely. This is an action: we don’t want to “get a new list,” we want to show the user the current data.

Until we move on to classes, we’ll store an expense as a Triple(category, amount, note) — it’s not ideal, but it honestly reflects the current stage of the course: we’re working with basic structures. Later it will become a separate type, and life will get easier.

Mini project data

fun main() {
    val expenses = mutableListOf(
        Triple("food", 250, "coffee"),
        Triple("transport", 120, "bus"),
        Triple("food", 560, "groceries"),
    )

    println("Expenses:")
    expenses.forEach { expense ->
        println(expense)
    }
    // Expenses:
    // (food, 250, coffee)
    // (transport, 120, bus)
    // (food, 560, groceries)
}

This already works, but the output is a bit “raw”: Triple prints as (a, b, c).

A bit better: format each line

Let’s make a small formatting function and apply it via forEach:

fun formatExpense(e: Triple<String, Int, String>): String {
    val (category, amount, note) = e
    return "$category: $amount ₣  ($note)"
}

fun main() {
    val expenses = listOf(
        Triple("food", 250, "coffee"),
        Triple("transport", 120, "bus"),
    )

    expenses.forEach { e ->
        println(formatExpense(e))
    }
    // food: 250 ₣  (coffee)
    // transport: 120 ₣  (bus)
}

Notice: forEach here is completely in its element. We made a “pure” function formatExpense, and used forEach for an action — printing the result.

Printing as a table: simple alignment

Since you already know padEnd() (and basic string work in general), you can make the output a bit nicer:

fun main() {
    val expenses = listOf(
        Triple("food", 250, "coffee"),
        Triple("transport", 120, "bus"),
    )

    expenses.forEach { (category, amount, note) ->
        val left = category.padEnd(10)
        println("$left | ${amount.toString().padStart(5)} | $note")
        // food       |   250 | coffee
        // transport  |   120 | bus
    }
}

Here we showed another nice trick: destructuring right in the lambda parameters. This makes code shorter and more readable when an element has multiple components.

“Don’t touch the collection you’re iterating over”

Since forEach iterates, many people feel tempted to do something inside like expenses.remove(...). From a common-sense point of view, that’s like “sawing off the branch you’re sitting on”: sometimes you’ll get an error, sometimes you’ll get weird behavior.

For this course, we can agree on this: inside forEach, we do not modify the collection we’re iterating over. For modifications, there are more suitable strategies (and separate discussions), and today we keep forEach as a tool to “walk through and perform an action.”

7. Common mistakes when working with forEach

Mistake #1: expecting forEach to build a new list.
A very common trap: “I’ll multiply numbers by forEach by 10 and get new numbers.” But forEach returns Unit, meaning it’s not about a result, it’s about an action. If you still manually build a new list via mutableListOf() and add(), it works, but you’re effectively writing a transformation using side effects. That’s not a disaster, but this code quickly becomes less readable, and already in the next lecture you’ll see tools that express the intent “get a new list” directly.

Mistake #2: hiding complex business logic inside forEach.
When you have 15 lines inside the lambda, three ifs, and you’re trying to remember how you got here — it’s time to move the code into a separate function. forEach is good when the lambda is either short or calls a clear function. Otherwise, reading turns into a quest: “find where the lambda ends and why it’s still inside it.”

Mistake #3: accumulating state through external variables and being surprised by bugs.
var sum = 0 and sum += x inside forEach is legal, but if there are several such external variables, you start writing a “mini program with hidden state.” Then you add an early return, an exception, or another pass — and the result becomes hard to explain. Side effects should be used like a sharp knife: for a purpose, and without waving it around.

Mistake #4: trying to use break/continue as in a regular loop.
forEach is not a language loop, so break inside it doesn’t work. Kotlin has options like labeled return (return@forEach), but that’s a separate topic, and for beginners it usually adds more confusion than value. If you need real “loop control” (stop, skip an iteration, exit on a condition) — it’s usually simpler and more honest to use a regular for.

Mistake #5: overusing it and losing the meaning of the parameter.
it is good when the lambda is short and obvious. But if inside you access multiple fields, format a string, and add conditions, it’s better to name the parameter (expense, name, x). Then the code starts to read like text instead of a puzzle. The Kotlin style guide explicitly supports this approach: short lambdas use it; nested/complex ones use explicit parameters.

1
Task
Kotlin SELF, level 22, lesson 0
Locked
Player greetings
Player greetings
1
Task
Kotlin SELF, level 22, lesson 0
Locked
List of words
List of words
1
Task
Kotlin SELF, level 22, lesson 0
Locked
Warning logs
Warning logs
1
Task
Kotlin SELF, level 22, lesson 0
Locked
Sum of payments
Sum of payments
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION