Kotlin Error Handling
Error handling is a crucial aspect of writing robust and reliable Kotlin applications. Proper error handling ensures that your program can gracefully respond to unexpected situations rather than crashing. In this guide, we'll explore various error handling techniques in Kotlin, from traditional approaches to more modern functional methods.
Introduction to Error Handling in Kotlin
When writing software, things don't always go according to plan. Files may be missing, network connections may fail, or user input might be invalid. Rather than allowing these issues to crash your application, well-designed error handling mechanisms help your program detect, report, and recover from errors.
Kotlin provides several approaches to error handling:
- Traditional try-catch blocks (inherited from Java)
- Extension functions for safer operations
- Functional approaches like
Result
type andrunCatching
- Null safety features to prevent null pointer exceptions
Let's explore each of these approaches with practical examples.
Traditional Try-Catch Error Handling
The most basic form of error handling in Kotlin uses the try-catch-finally pattern inherited from Java.
Basic Syntax
try {
// Code that might throw an exception
} catch (e: Exception) {
// Handle the exception
} finally {
// Code that will always execute, whether an exception is thrown or not
}
Example: File Reading
Here's an example of reading a file with try-catch:
import java.io.File
import java.io.FileNotFoundException
fun readFile(path: String): String {
try {
return File(path).readText()
} catch (e: FileNotFoundException) {
println("The file was not found: ${e.message}")
return ""
} catch (e: Exception) {
println("An error occurred: ${e.message}")
return ""
} finally {
println("Attempt to read file completed")
}
}
// Usage
fun main() {
val content = readFile("existing.txt")
println("Content: $content")
val missingContent = readFile("non-existing.txt")
println("Missing content: $missingContent")
}
Output:
Attempt to read file completed
Content: This is the content of the existing file.
The file was not found: non-existing.txt (No such file or directory)
Attempt to read file completed
Missing content:
Try as an Expression
In Kotlin, try-catch
can be used as an expression, meaning it can return a value:
val result = try {
parseInt(userInput)
} catch (e: NumberFormatException) {
null // Return null when an exception occurs
}
// result will either be the parsed integer or null
Kotlin-Specific Error Handling Features
Using require
, check
, and assert
Kotlin provides several functions that help validate conditions and throw appropriate exceptions:
require
: Validates arguments passed to a functioncheck
: Validates the state of an objectassert
: Validates assumptions during development (only active when JVM assertions are enabled)
fun processAge(age: Int) {
require(age > 0) { "Age must be positive" }
require(age < 150) { "Age must be realistic (< 150)" }
// Processing continues only if all requirements are met
println("Processing age: $age")
}
fun processAccount(account: BankAccount) {
check(account.isActive) { "Account must be active" }
// Processing continues only if all checks pass
println("Processing account: ${account.id}")
}
Usage example:
fun main() {
try {
processAge(25) // Works fine
processAge(-5) // Will throw an IllegalArgumentException
} catch (e: IllegalArgumentException) {
println("Invalid argument: ${e.message}")
}
}
Output:
Processing age: 25
Invalid argument: Age must be positive
Elvis Operator with throw
Kotlin's elvis operator (?:
) can be combined with throw
for concise null checks:
fun getEmployeeData(id: String?): EmployeeData {
val employeeId = id ?: throw IllegalArgumentException("Employee ID cannot be null")
// Process with non-null employeeId
return database.findEmployee(employeeId)
}
Functional Error Handling
Kotlin offers more modern, functional approaches to error handling that can lead to cleaner code.
The Result
Class
Introduced in Kotlin 1.3, the Result
class provides a container for success or failure:
fun parseJson(json: String): Result<JsonData> {
return try {
Result.success(JsonParser.parse(json))
} catch (e: JsonParseException) {
Result.failure(e)
}
}
// Usage
fun main() {
val result = parseJson("{\"name\":\"John\"}")
// Handle the result
result.fold(
onSuccess = { data -> println("Parsed data: $data") },
onFailure = { error -> println("Failed to parse: ${error.message}") }
)
// Or use other Result methods
if (result.isSuccess) {
val data = result.getOrNull()
println("Success with data: $data")
}
// Map and transform results
val mappedResult = result.map { it.name.uppercase() }
}
The runCatching
Extension
The runCatching
function makes it even easier to use Result:
fun fetchUserData(userId: String): UserData? {
return runCatching {
api.fetchUser(userId)
}.getOrNull()
}
// More comprehensive example
fun fetchAndProcessUser(userId: String): String {
return runCatching {
api.fetchUser(userId)
}.map { user ->
processUserData(user)
}.recover { error ->
when (error) {
is NetworkException -> "Network error: ${error.message}"
is UserNotFoundException -> "User not found"
else -> "Unknown error: ${error.message}"
}
}.getOrDefault("Could not process user")
}
The runCatching
function handles try-catch internally and returns a Result object.
Real-World Application: Building a Robust API Client
Let's put our knowledge into practice by building a simplified API client with proper error handling:
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.HttpURLConnection
import java.net.URL
sealed class ApiError : Exception() {
data class NetworkError(override val message: String, val cause: Throwable? = null) : ApiError()
data class ServerError(val statusCode: Int, override val message: String) : ApiError()
data class ClientError(val statusCode: Int, override val message: String) : ApiError()
data class UnknownError(override val message: String, val cause: Throwable? = null) : ApiError()
}
data class ApiResponse<T>(val data: T? = null, val error: ApiError? = null) {
val isSuccess: Boolean get() = error == null && data != null
}
class ApiClient {
suspend fun <T> fetchData(url: String, parser: (String) -> T): ApiResponse<T> {
return runCatching {
withContext(Dispatchers.IO) {
val connection = URL(url).openConnection() as HttpURLConnection
try {
connection.connectTimeout = 5000
connection.readTimeout = 5000
val statusCode = connection.responseCode
when (statusCode) {
in 200..299 -> {
val responseBody = connection.inputStream.bufferedReader().use { it.readText() }
ApiResponse(data = parser(responseBody))
}
in 400..499 -> ApiResponse(error = ApiError.ClientError(statusCode, "Client error"))
in 500..599 -> ApiResponse(error = ApiError.ServerError(statusCode, "Server error"))
else -> ApiResponse(error = ApiError.UnknownError("Unknown status code: $statusCode"))
}
} finally {
connection.disconnect()
}
}
}.getOrElse { throwable ->
ApiResponse(error = ApiError.NetworkError("Network error", throwable))
}
}
}
// Usage
suspend fun main() {
val apiClient = ApiClient()
val response = apiClient.fetchData("https://api.example.com/users") { responseBody ->
// Parse JSON here in a real app
responseBody.split(",")
}
when {
response.isSuccess -> {
println("Data received: ${response.data}")
}
response.error is ApiError.NetworkError -> {
println("Network error: ${response.error.message}")
// Maybe retry the request
}
response.error is ApiError.ServerError -> {
println("Server error (${(response.error as ApiError.ServerError).statusCode})")
// Maybe report to monitoring system
}
else -> {
println("Error: ${response.error?.message}")
}
}
}
This example demonstrates:
- Using sealed classes for typed errors
- Result-based error handling with
runCatching
- Proper resource cleanup with
finally
- Contextual error handling based on error type
Best Practices for Error Handling in Kotlin
-
Be specific with exceptions:
- Catch specific exception types rather than generic
Exception
when possible - Create custom exception types for your domain-specific errors
- Catch specific exception types rather than generic
-
Don't ignore exceptions:
- Always handle or propagate exceptions
- Avoid empty catch blocks except in rare, justified cases
-
Use functional approaches when appropriate:
Result
andrunCatching
lead to cleaner, more maintainable code- Combine with other functional features like
map
,flatMap
, andfold
-
Close resources properly:
- Use Kotlin's
use
function for resources that implementCloseable
- Don't rely on garbage collection for resource cleanup
- Use Kotlin's
-
Consider error handling as part of your API design:
- Provide meaningful error information to clients
- Document possible errors in function documentation
Summary
Kotlin provides a rich set of tools for error handling, from traditional try-catch blocks to functional approaches with the Result type. By combining these techniques appropriately, you can write robust code that gracefully handles errors while remaining readable and maintainable.
Key points to remember:
- Traditional try-catch works well for simple cases
- Kotlin's require, check, and assert functions help validate conditions
- The Result type and runCatching provide functional alternatives
- Sealed classes can represent typed errors
- Always clean up resources properly
Additional Resources
- Kotlin Official Documentation on Exceptions
- Arrow-kt Library - A functional companion to Kotlin's standard library
- Effective Java by Joshua Bloch - Contains excellent advice on exception handling that applies to Kotlin
Exercises
-
Create a function that reads a number from user input and handles potential number format exceptions using both try-catch and the Result approach.
-
Implement a resource manager class that safely handles file operations with proper error handling and resource cleanup.
-
Design a custom error hierarchy for a banking application, with specific exception types for different error scenarios (insufficient funds, account locked, etc.).
-
Refactor a function that uses multiple nested try-catch blocks to use the functional approach with Result.
Happy coding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)