JavaRush /Courses /Kotlin SELF /Getting to know exceptions

Getting to know exceptions

Kotlin SELF
Level 12, Lesson 1
Available

1. Compilation errors and runtime errors

If up to now you perceived errors as “red underlining in the IDE”, it is now important to separate two worlds. There are errors that Kotlin finds before launch (at compilation time), and there are problems that appear only while the program is running. Exceptions belong to the second type: they appear when the program is already running and encounters a situation it cannot continue with “as planned”.

For clarity, it is convenient to compare these things in a table:

What it is When it occurs What you see Example
Compilation error Before the program starts The IDE complains, kotlinc won’t compile
val x: Int = "hello"
Exception (runtime exception) During execution The program crashes; a message and a stack trace are printed
println(10 / 0)

An exception is not “just another if”. It is an emergency mechanism: like a fire alarm. If it goes off and nobody knows what to do, people (that is, the program’s execution) run to the exit (that is, the program terminates).

In Kotlin, exceptions are represented by objects that are subclasses of Throwable. In broad strokes: Throwable → Exception / Error. Usually, in application code we deal with Exception, while Error is more of a hundredry about serious runtime-environment issues (for example, running out of memory).

What an exception is

Imagine your program is a train traveling along rails made of lines of code. Normal control flow means the train follows the route: line 1, line 2, line 3… An exception is a situation where a concrete block suddenly appears on the rails: you cannot go further, and the train will not “carefully” roll over the obstacle. It hundredps, and control is handed over to a special error-handling mechanism.

Formally (and it is very important to remember this): when an exception is thrown, normal execution is interrupted. This literally means: “the next line after the problematic one will not execute” if the exception is not caught.

Mini-example — the classic “division by zero”:

fun main() {
    println("start")
    val a = 10
    val b = 0
    println(a / b)        // ArithmeticException
    println("end")        // will not execute
}

Note: the line println("end") looks harmless, but it will not be executed. Because the program “fell out” of the normal route at the line a / b.

2. An exception interrupts execution and prevents you from “going further”

With exceptions, beginners are most often confused by one thing: we are used to the idea that a program is a “list of commands”, and it executes them in order. And then it turns out that the order can be abruptly interrupted. So I will repeat the idea again, but in other words: an exception is not an “error message”; it is a change in control flow.

When an exception appears, the current function ends not via return, but abruptly. And then Kotlin/JVM tries to find a place that is ready to handle this exception. If there is no such place, the program terminates, and you see the error message and the call stack.

One more important detail: an exception may occur not where you wrote the “dangerous” code, but “deep” inside functions you called. For example, you call toInt(), and inside it Kotlin tries to parse the string and, if it fails, throws NumberFormatException. That exception “bubbles up” to your code.

Example:

fun main() {
    val text = "12a"
    val n = text.toInt()  // NumberFormatException
    println("n = $n")     // will not execute
}

So you didn’t create it, but the exception still appeared because the library function decided: “we can’t continue”.

3. Where exceptions come from in the first place

At this stage, it is useful not to try to memorize a hundred exception names, but to see the “types of situations” that lead to them. In simple learning programs, three families of issues pop up most often: arithmetic, string conversions, and array indexing.

Arithmetic: division by zero

With arithmetic, everything is intuitive: some operations are mathematically undefined. The JVM will not “guess” what you meant and will throw an exception.

fun main() {
    val total = 100
    val days = 0
    val perDay = total / days
    println(perDay) // we won't reach this
}

Converting a string to a number: “this is not a number”

You already know the safe path: toIntOrNull(). But toInt() is the “strict” variant. If the string is not a valid integer, a NumberFormatException occurs.

fun main() {
    val raw = "forty two"
    val n = raw.toInt()      // NumberFormatException
    println(n)
}

Array indexes: went out of bounds

Arrays have a fixed size, and indexes must be in the range 0..lastIndex. If not, you will get a runtime exception (on the JVM this is often ArrayIndexOutOfBoundsException).

fun main() {
    val xs = intArrayOf(10, 20, 30)
    println(xs[2])  // 30
    println(xs[5])  // ArrayIndexOutOfBoundsException
}

The psychological trap here is: “but the index is a number, I parsed it!”. Yes, a number. But not just any number — a valid one. And the exception is precisely saying: “the rule is violated”.

4. Exception hierarchy: Throwable, Exception, Error

When you see an error message, it almost always contains the exception type name. To avoid perceiving it as a set of random spells, it is useful to understand the top level of the hierarchy.

The root of the hierarchy is Throwable. It has two important “first-level descendants”: Exception and Error. The idea is: Exception is something that can, in principle, be part of normal program life (invalid input, resource unavailable, etc.), while Error is usually a serious environment problem that is pointless to “fix” at the level of your logic (for example, OutOfMemoryError).

At this stage, this scheme is enough for us:

flowchart TD
    T[Throwable]
    E[Exception]
    ER[Error]
    T --> E
    T --> ER
    E --> NFE[NumberFormatException]
    E --> AE[ArithmeticException]
    E --> AIOOBE[ArrayIndexOutOfBoundsException]

Important: this does not mean you must know the entire hierarchy by heart. But when you see ArithmeticException, you should think: “OK, this is an Exception, which means it’s a runtime-level problem in my scenario”.

Cause and location: the exception name and the stack trace

When a program crashes, a beginner often feels: “it broke somewhere”. But the exception mechanism is specifically designed to give you two hints: what happened (the cause) and where it happened (the location).

The cause is most often encoded in the type name and message. For example, NumberFormatException literally means: “the string does not match the number format”. The location is usually visible in the stack trace — the chain of function calls that led to the error. On an unhandled exception, Kotlin/JVM prints the stack trace automatically.

A mini-example that intentionally creates an error:

fun main() {
    throw ArithmeticException("Exception example")
}

Approximate output (it may look different, but the idea is the same):

Exception in thread "main" java.lang.ArithmeticException: Exception example
    at MainKt.main(Main.kt:2)

For now, you don’t need to be able to read a stack trace perfectly (we will learn that separately). But even now it’s useful to know: if the program crashed, it usually left a “receipt” showing where exactly.

5. Practical example

So that exceptions don’t remain an abstraction, let’s continue writing a small console application. Let it be called BudgetBuddy: the user enters expenses for several days, and the program calculates the total and the “average per day”.

For now, we will write as simply as possible, without exception handling (that will be the next lecture). Our goal here is to see exactly how the control flow breaks.

Naive version: “everything goes to plan”

Let’s start with a simple scenario: read the number of days, then read daily expenses into an array, then compute the sum.

fun main() {
    print("How many days? ")
    val days = readln().toInt()

    val expenses = IntArray(days)

    for (i in 0 until days) {
        print("Expense for day ${i + 1}: ")
        expenses[i] = readln().toInt()
    }

    var total = 0
    for (x in expenses) total += x

    println("Total: $total")
}

If the user enters normal numbers, everything is fine. But “normal numbers” are a pipe dream.

Crash scenario: the user entered not a number

Suppose the user enters the following:

  • “How many days?” → 3
  • “Expense for day 1” → 100
  • “Expense for day 2” → hundred (or 100r)

On the line readln().toInt(), a NumberFormatException will occur, and the program will terminate. Moreover, this will happen inside the loop, which means we won’t reach either the sum or the final println("Total...").

Crash scenario: the user entered 0 days (and we wanted an average)

Let’s add an average value. Naively:

fun main() {
    print("How many days? ")
    val days = readln().toInt()

    print("Total expenses: ")
    val total = readln().toInt()

    val avg = total / days
    println("Average per day: $avg")
}

If days = 0, on the line total / days there will be an ArithmeticException. And again: the line println(...) will not execute.

What is especially tricky here: even if days was parsed successfully (that is, it really is a number), it still can be invalid for your logic. Exceptions often occur not during parsing, but during use of the value.

Crash scenario: incorrect index handling

Imagine we want to show the “last entered expense”, but we made a mistake and used the index days instead of days - 1. (This is pain experienced by generations of programmers, and the tradition will continue.)

fun main() {
    val expenses = intArrayOf(100, 200, 300)
    val days = expenses.size

    println(expenses[days]) // days == 3, but indexes are 0..2
}

This will produce an ArrayIndexOutOfBoundsException. The program will again terminate abruptly.

What happens when an exception occurs: a mini-diagram

To reinforce the model, it helps to picture the program’s behavior as a diagram. Our program executes line by line as long as everything is fine. If a line throws an exception, an “emergency exit” occurs from the current function and the runtime walks up the call stack.

flowchart TD
    A[Execute line by line] --> B{Did an error occur?}
    B -->|no| A
    B -->|yes: exception| C[Interrupt the current function]
    C --> D[Search for a handler up the stack]
    D --> E{Handler found?}
    E -->|no| F[Print stack trace and terminate the program]
    E -->|yes| G[Transfer control to the handler]

For today, it’s enough to remember the branch “no handler found → the program terminated”. We will discuss the “handler found” branch in detail in the next lecture, when try/catch appears.

6. Typical mistakes

Mistake #1: confusing a compilation error with a runtime exception.
If the IDE underlines the code before you run it, that’s compilation: the program doesn’t even start. If the program started, managed to print something, and then suddenly “crashed” and showed a long text, that’s an exception, i.e., a runtime problem.

Mistake #2: expecting that code after the “problem line” will still execute.
This is the most common one. It seems like println("end") must be printed, since it is “after” the division. But an exception breaks linearity: execution is interrupted. In your head, you need to flip the switch: “exception = emergency exit”.

Mistake #3: thinking that if we used toIntOrNull(), then there will be no more exceptions.
toIntOrNull() really protects you from NumberFormatException, but it does not protect you from division by zero, going out of array bounds, or logical errors. It’s not “armor against everything”, but a tool against a specific cause.

Mistake #4: ignoring the exception name and reading the stack trace as a “scary wall of text”.
Even without debugging skills, the exception name is often very informative: NumberFormatException — not a number, ArithmeticException — arithmetic, ArrayIndexOutOfBoundsException — index out of bounds. Kotlin/JVM intentionally provides these hints.

Mistake #5: thinking an exception is “the end of the world”, so it’s better to “not touch it”.
In reality, exceptions are a normal working mechanism. The problem is not that they exist, but that we do not yet know how to handle them properly and turn them into understandable program behavior. That is exactly what we will do next: we will learn to catch expected exceptions and continue working, without turning the console into a drama theater.

1
Task
Kotlin SELF, level 12, lesson 1
Locked
Register for Two
Register for Two
1
Task
Kotlin SELF, level 12, lesson 1
Locked
Broken coupon
Broken coupon
1
Task
Kotlin SELF, level 12, lesson 1
Locked
Shelf in the warehouse
Shelf in the warehouse
1
Task
Kotlin SELF, level 12, lesson 1
Locked
Error Elevator
Error Elevator
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION