JavaRush/Courses/Kotlin SELF/try/catch for expected errors

try/catch for expected errors

Open

1. When do you need try/catch?

Honestly, it is a fair question: “Why do I need try/catch if I can write readln().toIntOrNull() and live peacefully?” And in many places, that is exactly what you should do. But exceptions have an unpleasant trait: they happen not only where you expect them, and you do not always have a “safe version” of an operation.

For example, toInt() with an invalid string throws an exception (instead of returning null). Dividing by zero throws an exception. An array index out of bounds does too. And if you want the program not to crash, you need a mechanism to “intercept the problem and continue”. That is exactly what try/catch does: “try to run dangerous code; if an expected error happens — do not crash, do something understandable instead”.

From Kotlin’s point of view, it looks like this: code inside try can be interrupted by an exception, and then control jumps to the matching catch.

Mental model: try/catch is not “one more if”

It is very important to build the right “picture in your head”. if is choosing a branch within the normal execution flow. You check a condition and carefully walk down the needed path. An exception is not a path; it is a trapdoor in the floor: you were walking, and then suddenly you fell into the basement.

try/catch is a way to put a safety net under that trapdoor. You are not saying “if there is an error — do this”, you are saying “try; if you fall — I will catch you”. That is why try/catch is especially useful where you cannot pre-check everything with a condition (or the check would be clunky), and where the cause of the problem is expressed precisely as an exception.

Syntax of try/catch and the exception object e

Let’s break down the form you will write most often. In Kotlin, the minimal way to handle an expected error is try { ... } catch (e: SomeException) { ... }. In try we put what can potentially “crash”, and in catch we put what we want to do instead of crashing the program.

The most “useful for a beginner” thing in catch is the variable e (usually named exactly that), the exception object. It has at least message (a string with the error text, sometimes null) and the exception type itself. At this stage you do not need to memorize the whole exception hierarchy, but it is important to understand one simple thing: the exception type is a hint about “what exactly went wrong”.

Mini-template:

fun main() {
    try {
        // here is what can "crash"
    } catch (e: Exception) {
        println("Something went wrong: ${e.message}")
    }
}

Just note: catching Exception right away is a “wide net”. Sometimes people do it, but today we are practicing a good habit: catch a specific expected error, not “everything”.

2. Example: string → number via toInt()

Let’s start with a classic situation: the user enters a number, but enters something that is not a number. Earlier we solved this via toIntOrNull(). Now we will intentionally use toInt() to see what a “crash” looks like, and how try/catch turns a crash into a normal message.

fun main() {
    print("Enter your age: ")
    val raw = readln()

    try {
        val age = raw.toInt()
        println("Age = $age") // for example: Age = 25
    } catch (e: NumberFormatException) {
        println("Error: this does not look like an integer") // if you entered "12a"
    }
}

It is important to notice the behavior here: if toInt() “crashes”, we do not execute the line println("Age = $age") — instead we jump into catch.

And one more small but practical point: do not try to “guess” e.message and show it to the user as the only text. Messages may be unfriendly. For the user, your text (“Enter an integer”) is usually better, and e.message is more of a hint for you.

Example: division and ArithmeticException

Now a situation where the user may enter a valid number, but an error still happens. Division by zero is the perfect example: “the input is valid, but semantically it does not fit”.

Let’s make a small fragment for our training app: we want to compute average daily spending, so we enter the total amount and the number of days.

fun main() {
    print("Total expenses: ")
    val total = readln().toIntOrNull() ?: 0

    print("Number of days: ")
    val days = readln().toIntOrNull() ?: 0

    try {
        val avg = total / days
        println("On average: $avg per day") // for example: On average: 100 per day
    } catch (e: ArithmeticException) {
        println("Error: you cannot divide by 0 days (makes sense, right?)")
    }
}

Why do we even need try/catch here if we used toIntOrNull()? Because the error happens not during string parsing, but during the arithmetic operation. You can “safely parse” 0 all you want, but dividing total / 0 is still division by zero.

3. The size of try

When a beginner first learns about try/catch, they typically have two extremes: either they do not use try anywhere at all, or they put one huge try around the entire main and catch everything. The second option looks convenient: “at least it won’t crash”. But that convenience has a price: you stop understanding what exactly crashed and where to look for the problem.

A good style when starting out: try should be minimal — wrap only the operation that can actually throw the expected exception at that point. The smaller the try, the less “magic”, the easier debugging is, and the more precisely you write handling.

Compare two variants. Bad (too wide):

fun main() {
    try {
        print("Enter a number: ")
        val n = readln().toInt()
        println("Square: ${n * n}")
        println("Done!")
    } catch (e: NumberFormatException) {
        println("Not a number")
    }
}

This code looks fine, but imagine that the error is not in toInt(), but somewhere later (not today, but in general). You would catch NumberFormatException even though the problem could be different.

A bit more careful (minimal try around toInt()):

fun main() {
    print("Enter a number: ")
    val raw = readln()

    val n = try {
        raw.toInt()
    } catch (e: NumberFormatException) {
        println("Not a number, using 0")
        0
    }

    println("Square: ${n * n}") // if you entered "abc" -> Square: 0
}

Formally, here try is used as an expression (it returns a value). Kotlin allows this: the result is taken from the last expression in try or in catch.

4. Pattern “catch → explain → continue”

The most practical benefit of try/catch in console programs is to make it so the user can make mistakes, and the program does not turn into “entered the wrong thing — goodbye”. To achieve this, we usually need a loop: ask, try to process, if there is an error — explain and repeat.

Let’s write a function readIntWithRetry(prompt: String): Int that asks for an integer and repeats input until it succeeds.

fun readIntWithRetry(prompt: String): Int {
    while (true) {
        print(prompt)
        val raw = readln()

        try {
            return raw.toInt()
        } catch (e: NumberFormatException) {
            println("An integer is required. Please try again.")
        }
    }
}

Pay attention to the style: in try we have exactly one “dangerous” operation — toInt(). If it succeeds, we return and exit the function. If not, we print a message and the loop repeats.

Now we can use it in main, and the code becomes cleaner:

fun main() {
    val total = readIntWithRetry("Total expenses: ")
    val days = readIntWithRetry("Number of days: ")
    println("You entered total=$total, days=$days")
}

5. Mini-app: expense tracking with resilient input

So that the examples are not just a bunch of separate snippets, let’s assemble a small console app that we can extend later in the course. Let it be an “expense piggy bank”: add expenses, show the total, compute the average (and do not crash).

We are not using when yet (it will come later), so we will implement commands via if/else. We will store data in an IntArray, because you already know arrays.

Storage stub and adding an expense

First, let’s sketch storage: an expenses array and a count counter. And a small add function.

fun addExpense(expenses: IntArray, count: Int, value: Int): Int {
    expenses[count] = value
    return count + 1
}

Yes, we are not checking for array overflow yet — just keep in mind that this is a learning example (and that the array is not infinite).

Total expenses

fun total(expenses: IntArray, count: Int): Int {
    var sum = 0
    var i = 0
    while (i < count) {
        sum += expenses[i]
        i++
    }
    return sum
}

Average value: division by zero is possible

Here we get a natural place for try/catch: average = total / count. If count == 0, we will divide by zero.

fun average(expenses: IntArray, count: Int): Int {
    val sum = total(expenses, count)

    return try {
        sum / count
    } catch (e: ArithmeticException) {
        0
    }
}

This example shows a useful real-world meaning of try/catch as an expression: we want to return a number on success or a “reasonable” 0 when it cannot be computed.

main: commands and resilient input

Now we make a simple command loop. Let the command list be: "add", "total", "avg", "exit".

fun main() {
    val expenses = IntArray(100)
    var count = 0

    while (true) {
        print("Command (add/total/avg/exit): ")
        val cmd = readln().trim()

        if (cmd == "add") {
            val value = readIntWithRetry("Enter an expense (integer): ")
            count = addExpense(expenses, count, value)
            println("Added: $value") // for example: Added: 150
        } else if (cmd == "total") {
            println("Total: ${total(expenses, count)}")
        } else if (cmd == "avg") {
            println("Average: ${average(expenses, count)}")
        } else if (cmd == "exit") {
            println("Bye!")
            return
        } else {
            println("Unknown command: $cmd")
        }
    }
}

What matters conceptually: the app no longer crashes because of trivial input like "qwe" instead of a number. It calmly says “An integer is required” and asks again. That is exactly “catch → explain → continue”.

6. Common mistakes when working with try/catch

Mistake #1: catching Exception “just in case”, without understanding what you actually expect.
It is tempting for the best reasons: “as long as it doesn’t crash”. But you turn errors into mush: the program will say “something went wrong”, and later you will not understand why. At the beginning the discipline is simple: if you expect “entered not a number”, catch NumberFormatException; if you expect division by zero — ArithmeticException. It is better to leave the generic Exception for later, when you already understand your handling strategy.

Mistake #2: making try too large and mixing input, computation, and output inside it.
A huge try creates a “black box” effect: something inside crashed, but you lose context. Also it is easy to accidentally catch an exception not where you truly expected it. Beginner practice: put one or two specific operations into try that can throw one specific expected error.

Mistake #3: an empty catch that silently “swallows” the error.
If you do nothing in catch, the program may keep running in a weird state: data was not read, a value was not computed, but externally “everything is ok”. That is very painful to debug later. Even if you do not know what to do, it is better to at least print a clear message to the user or to yourself.

Mistake #4: printing only ${e.message} to the user and hoping that this is UX.
Exception messages are often technical and sometimes even null. For the user, “Enter an integer” or “You cannot divide by zero” is better. And e.message is more of an auxiliary detail that you can add in parentheses if you have a debug mode.

Mistake #5: hoping that try/catch replaces validation.
try/catch helps, but it does not make code “smart”. For example, if you want to forbid negative expenses, that is not an exception — it is a business rule. It is better to check it with if (value < 0) and explain it to the user. Exceptions are for truly “emergency” runtime situations, not for regular logic.

Mistake #6: catching an exception and continuing, but forgetting to restore state.
If you started mutating variables and then something crashed, you can leave the program in a “half-broken” state. So a good style is: first obtain and validate input, then mutate state (for example, increment count). In our example, we first read value, and only then add it to the array — it is a small but important habit.

1
Task
Kotlin SELF,  12 level,  2 lecture
Locked
Square in chat
Square in chat
1
Task
Kotlin SELF,  12 level,  2 lecture
Locked
Budget splitting
Budget splitting
1
Task
Kotlin SELF,  12 level,  2 lecture
Locked
Input terminal
Input terminal
1
Task
Kotlin SELF,  12 level,  2 lecture
Locked
Showcase by index
Showcase by index
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet