Compile errors vs runtime exceptions
Kotlin checks your program twice. First kotlinc, the compiler, reads every file and refuses to produce bytecode if the grammar, the types or the null-safety rules are wrong. Kotlin's compiler catches much more than most languages' — null dereferences, missing when branches, reassigned vals — so a large share of beginner bugs never reach runtime. What is left runs on the JVM, and problems that only appear with real data (an index past the end, text that is not a number, a !! on a null) are thrown as exceptions.
Compile error (kotlinc)
- Nothing runs at all — not even the first line of
main - Format:
Main.kt:3:18: error: message(file, line, column), then the line with^^^under the exact token - In IntelliJ: red underline as you type; in Gradle: lines starting with
e: - Always reproducible: same code, same error
Runtime exception (JVM)
- The program started, maybe printed output, then stopped
- Format:
Exception in thread "main" java.lang.XxxException: message - Followed by
at MainKt.function(Main.kt:line)frames - Depends on the input: it may work for one value and crash for another
Anatomy of a kotlinc error
Main.kt:3:18: error: unresolved reference 'size' on receiver of type 'String'.
println(word.size)
^^^^File, line 3, column 18; the word error (warnings say warning); the message; then the source line with carets under the exact token. The message usually names both what you wrote and the type the compiler saw.
- Fix the first error first. One missing brace or parenthesis can produce a cascade; fix the first, recompile, and many of the rest vanish.
- Read the types in quotes.
expected 'Int', actual 'String'orreceiver of type 'String?'is usually the whole diagnosis. - Read warnings too. "Variable is never used", "Condition is always true" and "Unchecked cast" often point at the real bug. Many teams turn on
allWarningsAsErrorsin Gradle.
Anatomy of a stack trace
Exception in thread "main" java.lang.NumberFormatException: For input string: "x"
at java.base/java.lang.NumberFormatException.forInputString(Unknown Source)
at java.base/java.lang.Integer.parseInt(Unknown Source)
at java.base/java.lang.Integer.parseInt(Unknown Source)
at MainKt.parse(Main.kt:1)
at MainKt.total(Main.kt:5)
at MainKt.main(Main.kt:10)
at MainKt.main(Main.kt)The real trace of the program below without its try/catch. Top-level functions in Main.kt compile into a class called MainKt. The last frame, with no line number, is the JVM entry point Kotlin generates around your main.
- 1Read the first line
The exception class and message are often the whole answer: For input string: "x" means someone converted
"x"to a number. - 2Skip the library frames
Frames starting with
java.base/,kotlin.or a framework package are not your bug. Kotlin'stoInt()is a thin wrapper, so the JDK'sInteger.parseIntappears here. - 3Find the first frame in your code
MainKt.parse(Main.kt:1)is the line of your code that made the failing call. Open it. - 4Read downward to see how you got there
Each frame below is the caller:
totalline 5 calledparse, andmainline 10 calledtotal. The bad value came in along that path. - 5Jump to the last "Caused by"
Frameworks (Spring, Ktor, coroutines) wrap exceptions. The deepest
Caused by:section is the original failure. Lambdas show up as frames likeMainKt$main$1.invoke.
A stack trace is also an object you can inspect. This program catches the exception and prints only the frames from MainKt — exactly the filter your eyes should apply.
fun parse(s: String): Int = s.toInt()
fun total(items: List<String>): Int {
var t = 0
for (s in items) t += parse(s)
return t
}
fun main() {
try {
total(listOf("4", "x"))
} catch (e: NumberFormatException) {
println(e)
for (f in e.stackTrace) {
if (f.className == "MainKt" && f.lineNumber > 0) {
println(" at ${f.methodName} line ${f.lineNumber}")
}
}
}
}java.lang.NumberFormatException: For input string: "x"
at parse line 1
at total line 5
at main line 11Swap the list to listOf("x", "4"). Do the frames change? Why not?
