Table of Contents
- What Are Errors and Exceptions in Kotlin?
- The
try-catch-finallyBlock - Checked vs. Unchecked Exceptions in Kotlin
- Throw Expressions
- Custom Exceptions
- Try as an Expression
- Null Safety and Error Handling
- Resource Management with
use() - Best Practices for Error Handling
- Real-World Example
- Conclusion
- References
What Are Errors and Exceptions in Kotlin?
In Kotlin, errors are broadly categorized into two types:
- Errors: Critical issues (e.g.,
OutOfMemoryError,StackOverflowError) that indicate severe problems in the runtime environment. These are rare and usually cannot be recovered from. - Exceptions: Unexpected but recoverable events during program execution (e.g., division by zero, missing files). Exceptions are the focus of error handling.
All exceptions in Kotlin are subclasses of Throwable, which has two main subclasses:
Error: For unrecoverable issues (e.g.,VirtualMachineError).Exception: For recoverable issues. Most exceptions you’ll handle inherit from this (e.g.,ArithmeticException,NullPointerException).
The try-catch-finally Block
The try-catch-finally block is Kotlin’s primary mechanism for handling exceptions. It lets you:
try: Execute code that might throw an exception.catch: Handle specific exceptions if they occur.finally: Run cleanup code (e.g., closing resources) that executes always, regardless of whether an exception occurred.
Syntax
try {
// Code that might throw an exception
} catch (e: SpecificException) {
// Handle SpecificException
} catch (e: AnotherException) {
// Handle AnotherException (more general exceptions last!)
} finally {
// Cleanup code (runs always)
}
Example: Division by Zero
Let’s handle an ArithmeticException when dividing by zero:
fun divide(a: Int, b: Int): Int {
return try {
a / b // Risky operation: may throw ArithmeticException if b is 0
} catch (e: ArithmeticException) {
println("Error: ${e.message}") // Handle the exception
0 // Return a default value
} finally {
println("Division attempt completed.") // Runs always
}
}
fun main() {
println(divide(10, 2)) // Output: Division attempt completed. \n 5
println(divide(10, 0)) // Output: Error: / by zero \n Division attempt completed. \n 0
}
Key Notes:
- Catch blocks must order from most specific to most general exceptions (e.g.,
ArithmeticExceptionbeforeException). - The
finallyblock is optional but useful for releasing resources (e.g., closing files or network connections).
Checked vs. Unchecked Exceptions in Kotlin
Java requires declaring “checked exceptions” (e.g., IOException) in method signatures with throws, forcing callers to handle them. Kotlin eliminates checked exceptions entirely.
In Kotlin:
- All exceptions are unchecked (no need to declare
throws). - This reduces boilerplate but shifts responsibility to developers to handle exceptions appropriately.
Example: In Java, FileReader throws a checked FileNotFoundException, requiring a try-catch or throws declaration. In Kotlin, you can use FileReader without declaring exceptions, but you should still handle errors:
import java.io.FileReader
fun readFile() {
val reader = FileReader("nonexistent.txt") // May throw FileNotFoundException (unchecked in Kotlin)
// ...
}
Throw Expressions
In Kotlin, throw is an expression (not just a statement), meaning it returns a value of type Nothing (a special type representing “no value”). This lets you use throw in assignments, Elvis operators, or other expressions.
Example 1: Throw in an Assignment
fun getAge(input: String): Int {
return input.toIntOrNull() ?: throw IllegalArgumentException("Invalid age: $input")
}
fun main() {
getAge("25") // Returns 25
getAge("abc") // Throws IllegalArgumentException: Invalid age: abc
}
Example 2: Throw in a Function
fun validateUser(user: User?) {
user ?: throw NullPointerException("User cannot be null") // Throw if user is null
}
The Nothing type ensures the compiler knows code after throw is unreachable, preventing logic errors.
Custom Exceptions
For application-specific errors (e.g., “user not found”), define custom exceptions by extending Exception or RuntimeException. Use RuntimeException for unchecked exceptions (most common in Kotlin).
Example: Custom Exception
// Define a custom exception
class UserNotFoundException(message: String) : RuntimeException(message)
// Simulate a user database
class UserDatabase {
private val users = mapOf(1 to "Alice", 2 to "Bob")
fun getUser(id: Int): String {
return users[id] ?: throw UserNotFoundException("User with ID $id not found")
}
}
fun main() {
val db = UserDatabase()
try {
println(db.getUser(1)) // Output: Alice
println(db.getUser(99)) // Throws UserNotFoundException
} catch (e: UserNotFoundException) {
println("Error: ${e.message}") // Output: Error: User with ID 99 not found
}
}
Try as an Expression
Kotlin allows try blocks to return a value, making them expressions. This is useful for compact error handling.
Example: Try as an Expression
fun parseNumber(input: String): Int? {
return try {
input.toInt() // Success: return parsed Int
} catch (e: NumberFormatException) {
null // Failure: return null
}
}
fun main() {
val number = parseNumber("42") // number = 42
val invalid = parseNumber("not_a_number") // invalid = null
}
Here, the try block returns the result of input.toInt() on success, or null on failure.
Null Safety and Error Handling
Kotlin’s null safety features (e.g., nullable types ?, safe calls ?., Elvis operator ?:) work hand-in-hand with error handling to prevent NullPointerException (NPE).
Safe Calls (?.)
Use ?. to safely access nullable objects. If the object is null, the expression returns null instead of throwing an NPE:
val user: User? = null
val username = user?.name // username = null (no NPE)
Elvis Operator (?:)
The Elvis operator ?: provides a default value when an expression is null. Combine it with throw to fail fast on null:
val user: User? = null
val username = user?.name ?: throw IllegalArgumentException("User must not be null")
Resource Management with use()
For resources like files or network connections, Kotlin provides the use() extension function (similar to Java’s try-with-resources). use() ensures the resource is closed automatically after use, even if an exception occurs.
Example: Reading a File with use()
import java.io.BufferedReader
import java.io.FileReader
fun readFileSafely(path: String): String {
return BufferedReader(FileReader(path)).use { reader ->
reader.readText() // Read file content
} // reader is closed automatically here
}
fun main() {
try {
val content = readFileSafely("data.txt")
println(content)
} catch (e: Exception) {
println("Failed to read file: ${e.message}")
}
}
use() works with any AutoCloseable resource (e.g., FileReader, HttpClient), making resource cleanup concise and safe.
Best Practices for Error Handling
-
Catch Specific Exceptions
Avoid catching general exceptions likeExceptionorThrowable, as they may hide bugs (e.g.,NullPointerException). Catch specific exceptions instead:// Bad: Catches all exceptions, including bugs try { ... } catch (e: Exception) { ... } // Good: Catches only expected errors try { ... } catch (e: IOException) { ... } -
Avoid Empty
catchBlocks
Emptycatchblocks silently ignore errors, making debugging impossible. At minimum, log the error:catch (e: ArithmeticException) { println("Error: ${e.message}") // Or use a logging framework like Logcat } -
Prefer Non-Exceptional Flow for Expected Cases
Use exceptions for unexpected errors, not for control flow. For example, usetoIntOrNull()instead of catchingNumberFormatExceptionfor user input:// Better: Use nullability instead of exceptions for expected failures val age = input.toIntOrNull() ?: 0 -
Use
finallyoruse()for Cleanup
Always release resources (files, locks) infinallyor viause()to prevent leaks. -
Document Exceptions
Use KDoc to document which exceptions a function may throw, helping callers handle them:/** * Fetches a user by ID. * @throws UserNotFoundException if the user does not exist. */ fun getUser(id: Int): User { ... }
Real-World Example: User Service with Error Handling
Let’s combine custom exceptions, resource management, and null safety in a user service that reads user data from a file:
import java.io.BufferedReader
import java.io.FileReader
// Custom exception
class UserDataException(message: String) : RuntimeException(message)
// User data class
data class User(val id: Int, val name: String)
// Service to load users from a file
class UserService {
/**
* Loads a user from a CSV file.
* @param path Path to the CSV file (format: "id,name").
* @return User object.
* @throws UserDataException if the file is invalid or user is missing.
*/
fun loadUser(path: String): User {
return try {
BufferedReader(FileReader(path)).use { reader ->
val line = reader.readLine() ?: throw UserDataException("File is empty")
val parts = line.split(",").takeIf { it.size == 2 }
?: throw UserDataException("Invalid format: expected 'id,name'")
val id = parts[0].toIntOrNull() ?: throw UserDataException("Invalid ID: ${parts[0]}")
User(id, parts[1])
}
} catch (e: Exception) {
// Wrap lower-level exceptions (e.g., FileNotFoundException) in UserDataException
throw UserDataException("Failed to load user: ${e.message}")
}
}
}
fun main() {
val service = UserService()
try {
val user = service.loadUser("user.csv")
println("Loaded user: $user")
} catch (e: UserDataException) {
println("Error: ${e.message}")
}
}
This example:
- Uses
use()to auto-close theBufferedReader. - Throws custom
UserDataExceptionfor application-specific errors. - Validates input with null checks and
toIntOrNull(). - Wraps low-level exceptions (e.g.,
FileNotFoundException) to provide context.
Conclusion
Kotlin simplifies error handling with concise syntax, unchecked exceptions, and powerful features like try-as-expressions, use(), and null safety. By following best practices—catching specific exceptions, using finally/use() for cleanup, and leveraging null safety—you can write robust, maintainable code.
Start small: practice with try-catch, experiment with custom exceptions, and use use() for resources. Over time, error handling will become second nature!
References
- Kotlin Official Docs: Exceptions
- Kotlin in Action (Book by Dmitry Jemerov & Svetlana Isakova)
- Kotlin Null Safety Guide