JavaRush /Courses /Kotlin SELF /How to finish functions in Kotlin: return

How to finish functions in Kotlin: return

Kotlin SELF
Level 8, Lesson 2
Available

1. Why you need return if you can “just write code”

If you write all your code in main, it’s like telling a story without periods and paragraphs: it’s sort of understandable, but you get tired of reading quickly and it’s hard to find the point where “everything went wrong”. A function is a mini-story, and return is the period that ends the thought and sends the result outward. Without return, a value-computing function either won’t compile, or it will behave differently than you expect.

Imagine a real-life situation: you ask a friend, “calculate how much change I should get from 1000 USDT”. Your friend can: (1) answer with a number (“220”), or (2) just perform an action (for example, write that number on a piece of paper for you), or (3) wander off into endless thoughts about the meaning of money. In Kotlin, that’s roughly Int, Unit, and Nothing.

2. return and the function result type

When we write fun sum(a: Int, b: Int): Int, we are promising: “calling this function will produce an Int”. The compiler checks that promise, and it is fairly strict, because otherwise you’d get some interesting “surprises”.

So the key rule of today’s lecture is: if the signature specifies a result type (for example, : Int), then on any execution path the function must return a value of that type.

In practice, that turns into the following: you may exit the function early, you may have if/else, you may have a loop, but the compiler must be able to see that in the end you definitely return an Int. Otherwise, it will say something like “I can’t prove that you return a value”—and it will be right.

A small example of the correct approach:

fun sum(a: Int, b: Int): Int {
    return a + b
}

fun main() {
    val r = sum(10, 20)
    println(r) // 30
}

And here is an example that will not compile, because one branch returns nothing:

fun absBad(x: Int): Int {
    if (x < 0) {
        return -x
    }
    // Here the compiler will ask: what should be returned if x >= 0?
}

The correct version with an explicit return on all branching paths:

fun absInt(x: Int): Int {
    if (x < 0) {
        return -x
    }
    return x
}

fun main() {
    println(absInt(-7)) // 7
    println(absInt(7))  // 7
}

To reinforce this, it helps to imagine execution “arrows”. A function is a box, and return is the exit from the box:

flowchart TD
    A["Call absInt(x)"] --> B{ x < 0 ? }
    B -->|yes| C[return -x]
    B -->|no| D[return x]

3. Early return: less nesting, easier reading

With experience comes an unpleasant discovery: the more if inside if inside if, the more you want to close the laptop and go grow tomatoes.

Early return (also called a guard clause) is a style that makes code linear: first we filter out unsuitable cases and exit, and then we write the “normal scenario” without extra indentation.

Important: early return is not a “trick”, but a normal tool that makes code more readable. But it’s easy to overuse it if you turn a function into a set of exits “at every step”.

Let’s imagine a function that computes a discounted price. The discount must be in 0..100, otherwise we’ll end up owing money.

fun discountedPrice(price: Int, discountPercent: Int): Int {
    if (price < 0) return 0
    if (discountPercent < 0) return price
    if (discountPercent > 100) return 0

    val discount = price * discountPercent / 100
    return price - discount
}

fun main() {
    println(discountedPrice(1000, 25)) // 750
}

Notice the style: “bad cases—exit immediately”, “good case—at the end”. This reduces nesting and helps your brain follow the code without losing the thread.

And one more important point: return immediately ends the function. That means anything below return in the same branch will never run. Sometimes beginners expect return to mean “return a value, but keep going”, as if it were println. No: it is an exit.

4. Unit: action functions without a result

Sometimes a function returns nothing and performs an action: prints text, shows a menu, writes a log, increments an attempt counter. For such functions, Kotlin has the result type Unit. It is very similar to void from other languages, but technically it’s a real type—just with a single possible value (namely Unit itself).

Usually you can omit Unit in the signature, because Kotlin already understands: if you didn’t specify a result type, it’s Unit. Sometimes signatures with Unit appear in examples and documentation, for instance: fun foo(): Unit — the type is specified for clarity/demonstration.

An example of an action function:

fun printHeader(title: String) {
    println("==== $title ====")
}

fun main() {
    printHeader("Discount calculator")
    // ==== Discount calculator ====
}

There is nothing to return here; calling printHeader(...) is just a step in the scenario.

A helpful table to avoid confusion:

What the function does Result type Can be assigned to val x = ... Typical example
Computes a value
Int, String, Boolean, ...
Yes
val total = sum(1, 2)
Performs an action
Unit
Technically yes, but you almost never need to
printHeader("...")
Never returns
Nothing
No (because it does not return control) infinite loop

return in Unit functions

If a function returns Unit, that does not mean return is forbidden. On the contrary: return in a Unit function is a good way to do an early exit. It just looks a bit different: with no value. That is, just return, without a number/string.

This is useful when you want to “do nothing” in some situations, but you don’t want to wrap all useful code in a huge if.

Example: print a message only for positive numbers.

fun printIfPositive(x: Int) {
    if (x <= 0) return
    println("Positive: $x")
}

fun main() {
    printIfPositive(-3) // (prints nothing)
    printIfPositive(10) // Positive: 10
}

Another example: print a menu, but if the title is empty—exit without printing, because a “menu without a name” looks sad.

fun printMenu(title: String) {
    if (title.isBlank()) return

    println("== $title ==")
    println("1) Calculate discount")
    println("2) Exit")
}

fun main() {
    printMenu("Menu")
    // == Menu ==
    // 1) Calculate discount
    // 2) Exit
}

5. Nothing: a function that does not return control

The type Nothing sounds like a horror movie title: “Nothing: The Compiler Returns”. In practice, it’s a very interesting idea: a function with result type Nothing never completes normally. That is, it cannot return control to where it was called from.

It’s still too early for you to dive into the topics where this is a typical situation, so the clearest example of Nothing is an infinite loop.

Kotlin also uses Nothing as part of its type system: for example, in generics descriptions you may see constructs like Function<in Nothing, out Any?> — meaning “nothing can be safely passed in”, and that is expressed precisely as Nothing. For now, it’s enough to remember the idea: Nothing means “that’s it, the train does not go any further”.

The clearest example:

fun forever(): Nothing {
    while (true) {
        // We are stuck here forever.
        // In a real program, you should not do this.
    }
}

Why is this useful at all if “you should not do this”? Because Kotlin’s type system likes precision: if the compiler knows that “after this point execution will not continue”, it can analyze code and types better.

At your current level, the key human-friendly wording to remember is: Nothing is not an “empty value”, but a promise that “control will not return”.

6. Mini‑app

So the topic doesn’t stay abstract, let’s assemble a small console app that calculates a discounted price. We will deliberately split the logic into functions so that we can see all three variants: a computing function (Int), an action function (Unit), and an example function for Nothing (as a demonstration of the idea, not as part of a normal scenario).

Computing function: calculate the discount and return Int

First, we’ll write a pure computing function: it prints nothing, reads no input, and only calculates and returns a result. Such a function is easier to verify “in your head” and to reuse.

fun discountedPrice(price: Int, discountPercent: Int): Int {
    if (price < 0) return 0
    if (discountPercent < 0) return price
    if (discountPercent > 100) return 0

    val discount = price * discountPercent / 100
    return price - discount
}

fun main() {
    println(discountedPrice(1000, 25)) // 750
}

Here we already use early return, and that’s not accidental: bad values are easier to filter out immediately than to drag through the entire logic.

Action function: print the result

Now let’s add a function that only prints. It returns nothing outward; its purpose is to perform an action.

fun printResult(original: Int, discount: Int, result: Int) {
    println("Price: $original")
    println("Discount: $discount%")
    println("Total: $result")
}

fun main() {
    val result = discountedPrice(1000, 25)
    printResult(1000, 25, result)
    // Price: 1000
    // Discount: 25%
    // Total: 750
}

Notice: printResult does not return a value, but that does not make it weird. In programs, half of the functions are exactly like this: “do”, not “calculate”.

return without a value: gently exit printing if input is odd

Sometimes it’s better not to perform an action if the data is meaningless. Suppose we don’t want to print the result if the price is zero (for demonstration purposes—because a “discount on air” sounds philosophical).

fun printResultIfMeaningful(original: Int, discount: Int, result: Int) {
    if (original == 0) return
    println("Total after discount: $result")
}

fun main() {
    printResultIfMeaningful(0, 25, 0)      // (nothing)
    printResultIfMeaningful(1000, 25, 750) // Total after discount: 750
}

There it is: return without a value— a normal tool for Unit functions.

Assemble main: input, calculation, printing

At this point we have enough pieces to assemble a simple scenario. We are still without smart input and without error handling via exceptions—that will come later. Here it’s important to see how returned values “flow” from function to function.

fun main() {
    println("Enter the price:")
    val price = readln().toInt()

    println("Enter the discount (0..100):")
    val discount = readln().toInt()

    val result = discountedPrice(price, discount)
    printResult(price, discount, result)
}

Notice the composition: discountedPrice(...) returns an Int, we store it in result, and then pass it into printResult(...), which returns Unit and simply prints the result.

Demonstrating Nothing: “a function that won’t let go”

And finally, a small demonstration of Nothing. In a normal app you should not do this, but for understanding the type it’s useful to see it once.

fun freeze(): Nothing {
    while (true) {
        println("I am stuck in the loop and I will not return…")
    }
}

fun main() {
    println("Before freeze")
    freeze()
    println("After freeze") // this line will never run
}

You will never see “After freeze” in the console. And that is exactly what Nothing means: control does not return.

7. Typical mistakes

Mistake #1: not all branches return a value in a function with a result type.
The most common case is an if with a return inside, and then “somehow it’ll work out”. The compiler is not a telepath and is not required to guess. The fix is simple: either add a return after the if, or write a full if/else where both branches return the required type.

Mistake #2: trying to “return a value” from a Unit function.
Beginners sometimes write return 5 inside a function that is declared as fun f() { ... }. The compiler will complain, and it is right: either the function computes and must have : Int, or it is an action and returns Unit.

Mistake #3: expecting return to work like break or to “just end the if”.
return ends the function, not an if block and not a loop. If you write return inside a loop, you exit the function entirely. For loops we have other tools (break/continue).

Mistake #4: using Nothing as an “empty result”.
Nothing is not an “empty value” and not “return nothing”. It means “do not return at all”. If you see Nothing, think about an infinite loop (within today’s lecture) and about the fact that code after calling such a function will not execute.

Mistake #5: printing inside a computing function without a clear reason.
When a function both calculates and prints, it is harder to reuse: you just wanted a number, and it already printed something to the console. Sometimes that’s acceptable (especially in small scripts), but more often it’s better to separate concerns: one function returns a value, another prints (for example, via println). This makes the program more predictable and easier to debug.

1
Task
Kotlin SELF, level 8, lesson 2
Locked
Vibration sensor
Vibration sensor
1
Task
Kotlin SELF, level 8, lesson 2
Locked
Direction indicator
Direction indicator
1
Task
Kotlin SELF, level 8, lesson 2
Locked
Crash-free division
Crash-free division
1
Task
Kotlin SELF, level 8, lesson 2
Locked
Event poster
Event poster
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION