Kotlin / Language foundations and explicit state / Lesson 6
Separate validation, exceptions, and cleanup
Use require, check, try expressions, narrow catches, and finally while keeping expected input failures explicit.
On this page
Lesson goals
- Choose precondition enforcement or an ordinary invalid-input result according to the caller contract.
- Explain catch scope and cleanup without swallowing unrelated failures.
An exception is not the only way to express failure
Expected external conditions and broken internal assumptions deserve different handling. A user typing a malformed count is often an ordinary invalid-input outcome, while a function called with a forbidden argument may indicate a programming error. require checks an argument-related precondition and throws IllegalArgumentException on failure; check checks an illegal state and throws IllegalStateException. These are runtime checks, not compiler proofs. State the function’s accepted inputs and decide whether callers should branch on a result or treat misuse as exceptional before selecting syntax.
Reject misuse before producing a misleading result
The sample computes a reservation subtotal in synthetic minor currency units. It requires a positive seat count and a nonnegative unit amount. Those checks make the function contract visible, but ordinary Int multiplication can still overflow for sufficiently large inputs. A real money calculation needs justified ranges or a suitable exact representation. The catch in main is deliberately narrow and prints a fixed safe outcome. It does not expose an arbitrary stack trace or claim that every exception in an application should be converted into the same text.
fun subtotal(seats: Int, unitCents: Int): Int {
require(seats > 0) { "seats must be positive" }
require(unitCents >= 0) { "unit amount must be nonnegative" }
return seats * unitCents
}
fun main() {
println(subtotal(2, 150))
try {
subtotal(0, 150)
} catch (error: IllegalArgumentException) {
println("invalid argument")
}
}
300
invalid argument
Expected output is 300 and invalid argument. The demonstration inputs fit Int. The program was not compiled, and it implements no real payment or accounting operation.
A try expression can produce a value
try and catch blocks can provide a result used by an enclosing expression. Keep the protected block narrow enough that the caught exception has an unambiguous meaning. If parsing and database work share one broad catch, a storage defect may be mislabeled as invalid input. Prefer toIntOrNull for ordinary nullable parsing when the detailed exception is unnecessary. Catching Exception around every function can also hide programming defects and, in coroutine code, interfere with cancellation unless cancellation is preserved. Later lessons address that structured-concurrency boundary explicitly.
Finally handles cleanup rather than redefining the result
A finally block runs as control leaves the associated try construct under normal exception-handling semantics. Use it for necessary cleanup, and avoid returning from finally because that can obscure an earlier return or exception. On the JVM, resource-owning code often uses use for compatible closeable resources so closure is tied to scope; the exact resource type and platform API matter. Cleanup can itself fail, so diagnostics should preserve the important original failure. The example below has no file or network resource and demonstrates ordering with console output only.
fun parseOrFallback(text: String): Int = try {
text.toInt()
} catch (error: NumberFormatException) {
-1
} finally {
println("parse finished")
}
fun main() {
println(parseOrFallback("12"))
println(parseOrFallback("bad"))
}
parse finished
12
parse finished
-1
Expected lines are parse finished, 12, parse finished, -1. Finally prints before the caller prints the returned result. The fallback is a teaching sentinel and is unsuitable where -1 is a valid domain value.
Translate failures at the layer that understands them
A parsing boundary can explain malformed input, an application boundary can decide a retry or rejection, and a presentation boundary can choose safe wording. Avoid losing the cause by throwing a new unrelated exception without context or silently returning a plausible success. Logs should record minimal useful evidence without secrets or full personal payloads. Tests should inspect the expected result and state after failure, not merely verify that something was caught. This course’s examples use fixed local values and reasoned outputs; no filesystem cleanup, external provider failure, or JVM exception execution was tested here.
Common errors and fixes
Every failure becomes invalid input.
- Why it happens
- A catch block covers unrelated operations and exception types.
- How to fix it
- Narrow the protected block and translate only understood failures.
- Prevent it next time
- Keep parsing, domain policy, and external I/O boundaries distinct.
A finally return hides the original exception.
- Why it happens
- Cleanup changed control flow instead of preserving it.
- How to fix it
- Remove the return and keep cleanup side effects bounded.
- Prevent it next time
- Review cleanup paths with both success and failure cases.
A valid negative value is mistaken for parse failure.
- Why it happens
- A sentinel overlaps the permitted data range.
- How to fix it
- Return null or a structured outcome instead of an ambiguous number.
- Prevent it next time
- Check that failure representations cannot be valid data.
Check your understanding
1. Should a web form use require for every mistyped character?
Show worked solution
Usually the application should represent ordinary invalid input as a validation result and show useful feedback. require is appropriate when violating a function’s argument contract signals misuse by its caller. An input adapter can parse and validate first, then call a stricter internal function. Make the distinction explicit instead of selecting exceptions merely for shorter code.
2. Why is successful catch execution insufficient evidence of correct recovery?
Show worked solution
The program may have left partial state, lost the original cause, leaked a resource, or returned a misleading success. Verify the intended output, invariant, and resource state under the specific failure. For external actions, determine whether the operation completed before retrying. A local catch cannot undo an already committed remote effect.
Key takeaways
- Expected input failure, misuse, and invalid state need distinct contracts.
- Cleanup and error translation must preserve truthful outcomes.