1. finally: “cleanup” that happens in any scenario
When you write a program, you almost always mentally expect an “ideal world”: the user enters valid data, the computation works, we print the result, and we exit gracefully to applause. But the real world is when the user enters -999, then q, then 2147483648, and then gets upset that the program “can’t understand simple things.” The finally block is precisely about making sure your code correctly finishes important actions regardless of whether an error happened or not.
Formally, finally { ... } is a block that always executes: if try finishes successfully, if an exception occurs inside try, and even if the exception is caught in catch. This is the classic try/catch/finally construct, and Kotlin describes finally as a cleanup block—actions that must be performed no matter what.
Imagine an analogy: you are cooking in the kitchen. It does not matter whether the dish turns out well or you accidentally burn the pasta—you will still have to wash the dishes. That is what finally is: “wash the dishes,” not “fix the pasta.”
Mini-skeleton: try/catch/finally
fun main() {
try {
println("try: doing something useful")
println(10 / 2) // 5
} catch (e: ArithmeticException) {
println("catch: division error")
} finally {
println("finally: I will run no matter what")
}
}
If you change 10 / 2 to 10 / 0, the line from catch will run, but the line from finally will also run.
try { ... } finally { ... } without catch
Sometimes you do not need to handle the error right here. You need something else: to guarantee that an action is completed, and then let the error “bubble up” and crash the program (or get caught somewhere higher). This looks harsh, but in practice it can be useful even in small console programs: for example, you want to always print “Session finished” at the end, or reset temporary state, or close some resource.
Kotlin allows using try with a single finally without catch. In this case, finally will execute, and the exception—if it occurred—will continue on its way (that is, the program will still crash if nobody catches it). Kotlin also emphasizes that try must be accompanied by at least catch or finally—a “lonely try” makes no sense.
fun main() {
println("Start")
try {
val a = 10
val b = 0
println(a / b) // ArithmeticException
} finally {
println("Finishing: thanks for testing the program")
}
println("Finish") // will not execute
}
Notice an important detail: finally will run, but the line println("Finish") will not. In other words, finally does not “heal” the program. It just guarantees that you get a chance to do the required final action.
finally and return: finally runs, but does not “override” the result
At first, finally can feel like magic: “It always runs—so can it change everything?” No: finally has a different role. Kotlin specifically emphasizes that finally always executes, but does not change the result of the try/catch expression (whatever is returned from try or catch is the result).
This matters because you will soon start writing functions that return a value from try/catch. You need to understand the model: finally is a “side-effect finishing ritual,” not “another way to return a value.”
fun divideOrMinusOne(a: Int, b: Int): Int {
try {
return a / b
} catch (e: ArithmeticException) {
return -1
} finally {
println("finally: division attempt finished")
}
}
fun main() {
println(divideOrMinusOne(10, 2)) // finally: division attempt finished
// 5
println(divideOrMinusOne(10, 0)) // finally: division attempt finished
// -1
}
If this is your first time seeing return inside try/catch/finally, do not worry. The logic is simple: as soon as Kotlin knows what to return, it still runs finally before exiting the function.
2. throw: how to honestly say “you can’t do that”
Up to this point, exceptions have behaved like a “natural disaster”: division by zero, bad input—and the program crashes. But a developer is a crafty creature: sometimes we ourselves want to say “this is forbidden by our program’s rules” and stop execution so we do not continue in an invalid state.
That is what the throw keyword is for: it explicitly throws an exception. Kotlin shows it as directly as other languages do: throw IllegalArgumentException("...").
A useful mental model for a beginner: throw is not an “error message,” it is an emergency stop. You are not asking the program “please be more careful,” you are saying: “You can’t keep going—the bridge is gone.”
Example: a function accepts only a positive number
fun parsePositiveInt(text: String): Int {
val n = text.toInt()
if (n <= 0) {
throw IllegalArgumentException("Expected a number > 0, but got: $n")
}
return n
}
Here we are not “catching” an error. We are defining the contract of the function: it returns an Int, but only if the number is positive. If not, this is not a “normal scenario,” but a violation of the function’s usage rules.
An important nuance: throw is an expression of type Nothing
This sounds scarier than it really is. You have already seen the Nothing type: it means “this expression never returns a value.” Kotlin’s documentation shows a classic pattern where throw is convenient as a “hard fallback” in the Elvis operator ?:.
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
fun main() {
val name: String? = null
val nonNullName = name ?: fail("Name is required")
println(nonNullName) // we will not reach here
}
For now, treat this as a nice bonus: the compiler understands that after fail(...) execution does not continue, so the types “line up.”
How to choose an exception and write a message
When you first learn about throw, a temptation appears: “Oh! I’ll throw exceptions everywhere.” Do not rush to turn your code into a minefield. throw is good where you want to protect the meaning of a function: if the input data does not fit, it is better to stop and say so explicitly.
In learning tasks and small console programs, IllegalArgumentException is most commonly used: this exception literally means “a function argument is invalid.” Kotlin actively uses this type in examples about throw.
An exception message is your note to yourself (and future you). A good message usually has two parts: what was expected and what was received. Then, when it crashes, you do not have to guess why the program decided to stop.
fun divideStrict(a: Int, b: Int): Int {
if (b == 0) {
throw IllegalArgumentException("Divisor cannot be 0 (a=$a, b=$b)")
}
return a / b
}
Yes, you could simply try to divide and catch ArithmeticException. But sometimes it is better to check and throw your own clear exception. There is no single right answer here—what matters is that the behavior is understandable.
3. Mini-app: division with predictable behavior
Now for the most practical part: we will build a small console app that does one thing (division), but does it properly: it reads input carefully, checks rules, tells the user what is wrong, and always prints a final line at the end of an attempt. This is the style that will be very useful later when you start writing input and validation functions (yes, that will be a separate course level—today we are only laying the foundation).
Step 1: read a number or null
fun readIntOrNull(prompt: String): Int? {
print(prompt)
val raw = readln().trim()
return raw.toIntOrNull()
}
Nothing magical here: trim() removes extra spaces, and toIntOrNull() returns either an Int or null.
Step 2: strict division with throw
fun divideStrict(a: Int, b: Int): Int {
if (b == 0) throw IllegalArgumentException("Division by 0 is not allowed")
return a / b
}
This is our “contract”: a divisor of 0 is forbidden by the rules, so we explicitly say “you can’t do that.”
Step 3: main — catch the expected error and always finalize
fun main() {
val a = readIntOrNull("Enter a: ")
val b = readIntOrNull("Enter b: ")
try {
if (a == null || b == null) throw IllegalArgumentException("You must enter two integers")
val result = divideStrict(a, b)
println("a / b = $result") // for example: a / b = 5
} catch (e: IllegalArgumentException) {
println("Input/rule error: ${e.message}")
} finally {
println("Done: calculation attempt finished")
}
}
What is important to notice here (this is a key skill for this level):
try contains only what can actually “break” within our logic: rule checks, the divideStrict(...) call, and printing the result. If everything succeeds, catch is skipped, but finally still executes. If we throw an IllegalArgumentException (ourselves or from divideStrict), control jumps to catch, and then finally still executes.
Execution flow diagram
When you are just starting, it is easy to turn try/catch/finally into mental mush: “where does control jump and what runs?” A simple flowchart helps. Imagine the program is a train, and an exception is the emergency brake.
flowchart TD
A[Enter try] --> B{Did an exception occur in try?}
B -- no --> C[Run try code to the end]
B -- yes --> D[Jump to a matching catch]
C --> E[Run finally]
D --> E[Run finally]
E --> F{Is the exception handled?}
F -- yes --> G[Continue after try/catch/finally]
F -- no --> H["The exception 'bubbles up'; the program may crash"]
This diagram is not about “JVM internals,” it is about your daily practice: understanding why the next line does not execute, but finally does.
4. Common mistakes when working with finally and throw
Mistake #1: trying to “handle” an error in finally.
Sometimes a beginner thinks: since finally always runs, that is where “rescue logic” should go. But finally is for something else: you do not fix the problem there, you clean up. If you start parsing input or changing important data in finally, the code becomes unpredictable: the error has already happened, and you are adding another layer of surprises.
Mistake #2: putting code in finally that can crash by itself.
It sounds like a joke, but in practice it happens all the time: in finally people print something, access a variable that may not have been initialized, or do a division “for logging.” As a result, you lose the original cause of the error because a new one is layered on top. A good finally is usually short, simple, and as reliable as possible.
Mistake #3: using throw as a replacement for normal user-scenario checks.
If the user enters something wrong, that is most often an expected situation. You can handle it gently: show a message and ask them to try again. throw is more useful for violating function rules (contracts), when continuing execution is genuinely risky. At our level, it is fine to throw an exception for “division by zero is not allowed,” but if you start throwing exceptions for every typo, the program will constantly “explode.”
Mistake #4: throwing an exception without a meaningful message.
An exception without a message is like a note saying “everything is bad” with no details. Kotlin allows throwing IllegalArgumentException() without text, but then you will have fewer clues during debugging. It is much better to be specific: what you expected and what you got. Kotlin’s official examples do exactly that.
Mistake #5: thinking that finally will “continue the program” after a crash.
finally will run, yes. But if the exception is not caught (or you use try/finally without catch), then after finally execution does not turn back into the normal scenario: the exception still propagates upward. Kotlin shows that finally guarantees that final code runs, but it does not promise that the program will keep going if the error is not handled.