Exception Handling
Overview
Exception handling is an important guarantee of program robustness. Kotlin provides comprehensive exception handling mechanisms, including try-catch statements, custom exceptions, and the Result type. This chapter will detail how to elegantly handle errors and exceptions in Kotlin.
Exception Basics
try-catch-finally Statement
kotlin
import java.io.File
import java.io.FileNotFoundException
fun main() {
// Basic exception handling
fun divide(a: Int, b: Int): Double {
return try {
a.toDouble() / b
} catch (e: ArithmeticException) {
println("Arithmetic exception: ${e.message}")
0.0
}
}
println("10 / 2 = ${divide(10, 2)}")
println("10 / 0 = ${divide(10, 0)}")
// Multiple catch blocks
fun parseNumber(str: String): Int? {
return try {
str.toInt()
} catch (e: NumberFormatException) {
println("Number format error: $str")
null
} catch (e: Exception) {
println("Other exception: ${e.message}")
null
}
}
println("Parse '123': ${parseNumber("123")}")
println("Parse 'abc': ${parseNumber("abc")}")
// try-catch-finally
fun readFileContent(filename: String): String? {
var content: String? = null
try {
val file = File(filename)
content = file.readText()
println("File read successfully")
} catch (e: FileNotFoundException) {
println("File not found: $filename")
} catch (e: Exception) {
println("Error reading file: ${e.message}")
} finally {
println("Cleaning up resources")
}
return content
}
readFileContent("nonexistent.txt")
}try as Expression
kotlin
fun main() {
// try as expression
fun safeParseInt(str: String): Int {
val result = try {
str.toInt()
} catch (e: NumberFormatException) {
-1 // Default value
}
return result
}
println("Parse '42': ${safeParseInt("42")}")
println("Parse 'invalid': ${safeParseInt("invalid")}")
// Complex try expression
fun processData(data: String): String {
return try {
val number = data.toInt()
when {
number > 0 -> "Positive: $number"
number < 0 -> "Negative: $number"
else -> "Zero"
}
} catch (e: NumberFormatException) {
try {
val double = data.toDouble()
"Float: $double"
} catch (e: NumberFormatException) {
"Invalid data: $data"
}
}
}
println(processData("42"))
println(processData("-10"))
println(processData("3.14"))
println(processData("hello"))
}Exception Types
Standard Exceptions
kotlin
fun main() {
// Common standard exceptions
// NullPointerException (KotlinNullPointerException)
fun demonstrateNPE() {
val str: String? = null
try {
println(str!!.length) // Force unwrap null value
} catch (e: KotlinNullPointerException) {
println("Null pointer exception: ${e.message}")
}
}
// IndexOutOfBoundsException
fun demonstrateIndexOutOfBounds() {
val list = listOf(1, 2, 3)
try {
println(list[5])
} catch (e: IndexOutOfBoundsException) {
println("Index out of bounds: ${e.message}")
}
}
// IllegalArgumentException
fun validateAge(age: Int) {
if (age < 0 || age > 150) {
throw IllegalArgumentException("Age must be between 0-150, actual value: $age")
}
println("Age validation passed: $age")
}
// IllegalStateException
class BankAccount(private var balance: Double) {
fun withdraw(amount: Double) {
if (balance < amount) {
throw IllegalStateException("Insufficient balance, current: $balance, attempted: $amount")
}
balance -= amount
println("Withdrawal successful, balance: $balance")
}
}
println("=== Exception Demonstrations ===")
demonstrateNPE()
demonstrateIndexOutOfBounds()
try {
validateAge(-5)
} catch (e: IllegalArgumentException) {
println("Argument exception: ${e.message}")
}
val account = BankAccount(100.0)
try {
account.withdraw(150.0)
} catch (e: IllegalStateException) {
println("State exception: ${e.message}")
}
}Custom Exceptions
kotlin
// Custom exception classes
class ValidationException(message: String, cause: Throwable? = null) : Exception(message, cause)
class NetworkException(message: String, val errorCode: Int) : Exception(message)
class BusinessLogicException(
message: String,
val errorType: ErrorType,
val details: Map<String, Any> = emptyMap()
) : Exception(message)
enum class ErrorType {
INVALID_INPUT,
RESOURCE_NOT_FOUND,
PERMISSION_DENIED,
BUSINESS_RULE_VIOLATION
}
// Service class using custom exceptions
class UserService {
private val users = mutableMapOf<String, User>()
data class User(val id: String, val name: String, val email: String, val age: Int)
fun createUser(id: String, name: String, email: String, age: Int): User {
// Validate input
if (id.isBlank()) {
throw ValidationException("User ID cannot be empty")
}
if (name.isBlank()) {
throw ValidationException("Username cannot be empty")
}
if (!email.contains("@")) {
throw ValidationException("Invalid email format: $email")
}
if (age < 0 || age > 150) {
throw ValidationException("Age must be between 0-150: $age")
}
// Check business rules
if (users.containsKey(id)) {
throw BusinessLogicException(
"User ID already exists",
ErrorType.BUSINESS_RULE_VIOLATION,
mapOf("existingUserId" to id)
)
}
val user = User(id, name, email, age)
users[id] = user
return user
}
fun getUser(id: String): User {
return users[id] ?: throw BusinessLogicException(
"User does not exist",
ErrorType.RESOURCE_NOT_FOUND,
mapOf("requestedUserId" to id)
)
}
fun updateUserEmail(id: String, newEmail: String): User {
val user = getUser(id) // May throw exception
if (!newEmail.contains("@")) {
throw ValidationException("Invalid email format: $newEmail")
}
val updatedUser = user.copy(email = newEmail)
users[id] = updatedUser
return updatedUser
}
}
fun main() {
val userService = UserService()
// Successfully create user
try {
val user1 = userService.createUser("1", "Alice", "alice@example.com", 25)
println("User created successfully: $user1")
} catch (e: ValidationException) {
println("Validation failed: ${e.message}")
} catch (e: BusinessLogicException) {
println("Business logic error: ${e.message}, type: ${e.errorType}")
}
// Validation exception
try {
userService.createUser("", "Bob", "invalid-email", -5)
} catch (e: ValidationException) {
println("Validation failed: ${e.message}")
}
// Business logic exception
try {
userService.createUser("1", "Charlie", "charlie@example.com", 30)
} catch (e: BusinessLogicException) {
println("Business logic error: ${e.message}")
println("Error details: ${e.details}")
}
// Resource not found exception
try {
userService.getUser("999")
} catch (e: BusinessLogicException) {
println("${e.errorType}: ${e.message}")
}
}Result Type
Error Handling with Result
kotlin
// Safe operations using Result type
class SafeCalculator {
fun divide(a: Double, b: Double): Result<Double> {
return if (b != 0.0) {
Result.success(a / b)
} else {
Result.failure(ArithmeticException("Divisor cannot be zero"))
}
}
fun sqrt(x: Double): Result<Double> {
return if (x >= 0) {
Result.success(kotlin.math.sqrt(x))
} else {
Result.failure(IllegalArgumentException("Cannot calculate square root of negative number"))
}
}
fun factorial(n: Int): Result<Long> {
return when {
n < 0 -> Result.failure(IllegalArgumentException("Factorial argument cannot be negative"))
n > 20 -> Result.failure(ArithmeticException("Factorial result too large, exceeds Long range"))
else -> {
var result = 1L
for (i in 1..n) {
result *= i
}
Result.success(result)
}
}
}
}
// Chain operations with Result
class DataProcessor {
fun parseNumber(str: String): Result<Double> {
return try {
Result.success(str.toDouble())
} catch (e: NumberFormatException) {
Result.failure(e)
}
}
fun processChain(input: String): Result<String> {
return parseNumber(input)
.mapCatching { number ->
if (number < 0) throw IllegalArgumentException("Number cannot be negative")
number
}
.mapCatching { number ->
kotlin.math.sqrt(number)
}
.mapCatching { sqrt ->
"Square root result: ${"%.2f".format(sqrt)}"
}
}
}
fun main() {
val calculator = SafeCalculator()
val processor = DataProcessor()
println("=== Result Type Examples ===")
// Successful calculation
val divisionResult = calculator.divide(10.0, 2.0)
divisionResult.fold(
onSuccess = { result -> println("Division result: $result") },
onFailure = { error -> println("Division error: ${error.message}") }
)
// Failed calculation
val divisionByZero = calculator.divide(10.0, 0.0)
when {
divisionByZero.isSuccess -> println("Result: ${divisionByZero.getOrNull()}")
divisionByZero.isFailure -> println("Error: ${divisionByZero.exceptionOrNull()?.message}")
}
// Square root calculation
listOf(16.0, -4.0, 25.0).forEach { number ->
calculator.sqrt(number).fold(
onSuccess = { result -> println("√$number = $result") },
onFailure = { error -> println("√$number error: ${error.message}") }
)
}
// Factorial calculation
listOf(5, -3, 25).forEach { number ->
val result = calculator.factorial(number)
println("$number! = ${result.getOrElse { "Error: ${it.message}" }}")
}
println("\n=== Chain Operation Examples ===")
// Chain processing
listOf("16", "-4", "abc", "25").forEach { input ->
val result = processor.processChain(input)
println("Input '$input': ${result.getOrElse { "Error: ${it.message}" }}")
}
}Advanced Result Usage
kotlin
// Extension functions to enhance Result functionality
inline fun <T, R> Result<T>.flatMap(transform: (T) -> Result<R>): Result<R> {
return when {
isSuccess -> transform(getOrThrow())
else -> Result.failure(exceptionOrNull()!!)
}
}
fun <T> Result<T>.orElse(defaultValue: T): T {
return getOrElse { defaultValue }
}
fun <T> Result<T>.orElseGet(supplier: () -> T): T {
return getOrElse { supplier() }
}
// Batch Result operations
fun <T> List<Result<T>>.sequence(): Result<List<T>> {
val results = mutableListOf<T>()
for (result in this) {
when {
result.isSuccess -> results.add(result.getOrThrow())
result.isFailure -> return Result.failure(result.exceptionOrNull()!!)
}
}
return Result.success(results)
}
// Practical application: File processing service
class FileProcessingService {
fun readFile(filename: String): Result<String> {
return try {
// Simulate file reading
when (filename) {
"config.txt" -> Result.success("app.name=MyApp\napp.version=1.0")
"data.json" -> Result.success("""{"users": [{"name": "Alice"}, {"name": "Bob"}]}""")
else -> Result.failure(java.io.FileNotFoundException("File not found: $filename"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
fun parseConfig(content: String): Result<Map<String, String>> {
return try {
val config = content.lines()
.filter { it.contains("=") }
.associate { line ->
val parts = line.split("=", limit = 2)
parts[0].trim() to parts[1].trim()
}
Result.success(config)
} catch (e: Exception) {
Result.failure(IllegalArgumentException("Invalid config format", e))
}
}
fun validateConfig(config: Map<String, String>): Result<Map<String, String>> {
return when {
!config.containsKey("app.name") ->
Result.failure(ValidationException("Missing required config: app.name"))
!config.containsKey("app.version") ->
Result.failure(ValidationException("Missing required config: app.version"))
config["app.name"]?.isBlank() == true ->
Result.failure(ValidationException("app.name cannot be empty"))
else -> Result.success(config)
}
}
fun processConfigFile(filename: String): Result<Map<String, String>> {
return readFile(filename)
.flatMap { content -> parseConfig(content) }
.flatMap { config -> validateConfig(config) }
}
}
fun main() {
val fileService = FileProcessingService()
println("=== File Processing Service Example ===")
// Process different files
listOf("config.txt", "data.json", "nonexistent.txt").forEach { filename ->
println("\nProcessing file: $filename")
val result = fileService.processConfigFile(filename)
result.fold(
onSuccess = { config ->
println("Config loaded successfully:")
config.forEach { (key, value) -> println(" $key = $value") }
},
onFailure = { error ->
println("Processing failed: ${error.message}")
error.cause?.let { cause ->
println("Cause: ${cause.message}")
}
}
)
}
println("\n=== Batch Operation Example ===")
// Batch file reading
val filenames = listOf("config.txt", "data.json", "missing.txt")
val readResults = filenames.map { fileService.readFile(it) }
// Check if all files were read successfully
val allFilesResult = readResults.sequence()
allFilesResult.fold(
onSuccess = { contents ->
println("All files read successfully:")
contents.forEachIndexed { index, content ->
println("${filenames[index]}: ${content.take(50)}...")
}
},
onFailure = { error ->
println("Batch read failed: ${error.message}")
}
)
// Process only successful results
val successfulReads = readResults.mapNotNull { it.getOrNull() }
println("Number of files read successfully: ${successfulReads.size}")
}Exception Handling Best Practices
1. Exception Handling Strategies
kotlin
// Exception handling strategy examples
class OrderService {
// Fail-fast strategy
fun validateOrder(order: Order) {
require(order.items.isNotEmpty()) { "Order cannot be empty" }
require(order.customerId.isNotBlank()) { "Customer ID cannot be empty" }
require(order.totalAmount > 0) { "Order amount must be greater than 0" }
}
// Graceful degradation strategy
fun calculateShippingCost(order: Order): Double {
return try {
// Call external service to calculate shipping
callExternalShippingService(order)
} catch (e: NetworkException) {
// Use default shipping on network exception
println("Shipping service unavailable, using default shipping")
10.0
} catch (e: Exception) {
// Free shipping on other exceptions
println("Shipping calculation failed, free shipping")
0.0
}
}
// Retry strategy
fun processPayment(order: Order, maxRetries: Int = 3): Result<PaymentResult> {
var lastException: Exception? = null
repeat(maxRetries) { attempt ->
try {
val result = callPaymentService(order)
return Result.success(result)
} catch (e: NetworkException) {
lastException = e
println("Payment attempt ${attempt + 1} failed, ${maxRetries - attempt - 1} retries remaining")
if (attempt < maxRetries - 1) {
Thread.sleep(1000 * (attempt + 1)) // Exponential backoff
}
} catch (e: Exception) {
// Don't retry non-network exceptions
return Result.failure(e)
}
}
return Result.failure(lastException ?: Exception("Payment processing failed"))
}
private fun callExternalShippingService(order: Order): Double {
// Simulate external service call
if (kotlin.random.Random.nextBoolean()) {
throw NetworkException("Shipping service connection timeout", 408)
}
return order.totalAmount * 0.1
}
private fun callPaymentService(order: Order): PaymentResult {
// Simulate payment service call
val random = kotlin.random.Random.nextDouble()
when {
random < 0.1 -> throw NetworkException("Payment service unavailable", 503)
random < 0.2 -> throw Exception("Payment rejected")
else -> return PaymentResult("SUCCESS", "TXN_${System.currentTimeMillis()}")
}
}
}
data class Order(
val id: String,
val customerId: String,
val items: List<OrderItem>,
val totalAmount: Double
)
data class OrderItem(val productId: String, val quantity: Int, val price: Double)
data class PaymentResult(val status: String, val transactionId: String)
fun main() {
val orderService = OrderService()
val order = Order(
id = "ORD001",
customerId = "CUST001",
items = listOf(
OrderItem("PROD001", 2, 25.0),
OrderItem("PROD002", 1, 50.0)
),
totalAmount = 100.0
)
// Validate order
try {
orderService.validateOrder(order)
println("Order validation passed")
} catch (e: IllegalArgumentException) {
println("Order validation failed: ${e.message}")
return
}
// Calculate shipping (graceful degradation)
val shippingCost = orderService.calculateShippingCost(order)
println("Shipping cost: $shippingCost")
// Process payment (retry strategy)
val paymentResult = orderService.processPayment(order)
paymentResult.fold(
onSuccess = { result ->
println("Payment successful: ${result.transactionId}")
},
onFailure = { error ->
println("Payment failed: ${error.message}")
}
)
}2. Resource Management
kotlin
import java.io.Closeable
// Automatic resource management
inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
when {
this == null -> {}
exception == null -> close()
else -> {
try {
close()
} catch (closeException: Throwable) {
exception.addSuppressed(closeException)
}
}
}
}
}
// Resource management examples
class DatabaseConnection : Closeable {
private var isOpen = true
fun executeQuery(sql: String): List<Map<String, Any>> {
if (!isOpen) throw IllegalStateException("Connection is closed")
println("Executing query: $sql")
// Simulate query results
return listOf(
mapOf("id" to 1, "name" to "Alice"),
mapOf("id" to 2, "name" to "Bob")
)
}
override fun close() {
if (isOpen) {
println("Closing database connection")
isOpen = false
}
}
}
class FileWriter(private val filename: String) : Closeable {
private var isOpen = true
private val content = mutableListOf<String>()
fun writeLine(line: String) {
if (!isOpen) throw IllegalStateException("File is closed")
content.add(line)
println("Writing: $line")
}
override fun close() {
if (isOpen) {
println("Saving file: $filename")
println("Content: ${content.joinToString("\n")}")
isOpen = false
}
}
}
fun main() {
println("=== Resource Management Examples ===")
// Database connection auto-management
try {
DatabaseConnection().use { connection ->
val results = connection.executeQuery("SELECT * FROM users")
println("Query results: $results")
// If exception thrown here, connection will still be closed
if (results.isEmpty()) {
throw RuntimeException("No data found")
}
}
} catch (e: Exception) {
println("Database operation exception: ${e.message}")
}
// File writing auto-management
try {
FileWriter("output.txt").use { writer ->
writer.writeLine("First line")
writer.writeLine("Second line")
// Simulate exception
if (kotlin.random.Random.nextBoolean()) {
throw RuntimeException("Error during writing")
}
writer.writeLine("Third line")
}
} catch (e: Exception) {
println("File writing exception: ${e.message}")
}
println("Program ended")
}3. Error Propagation and Transformation
kotlin
// Error propagation and transformation examples
sealed class AppError(message: String, cause: Throwable? = null) : Exception(message, cause) {
class ValidationError(message: String) : AppError(message)
class NetworkError(message: String, cause: Throwable? = null) : AppError(message, cause)
class DatabaseError(message: String, cause: Throwable? = null) : AppError(message, cause)
class BusinessLogicError(message: String) : AppError(message)
}
class UserRepository {
fun findUser(id: String): Result<User> {
return try {
// Simulate database query
when (id) {
"1" -> Result.success(User("1", "Alice", "alice@example.com"))
"2" -> Result.success(User("2", "Bob", "bob@example.com"))
else -> Result.failure(AppError.DatabaseError("User not found: $id"))
}
} catch (e: Exception) {
Result.failure(AppError.DatabaseError("Database query failed", e))
}
}
}
class EmailService {
fun sendEmail(to: String, subject: String, body: String): Result<Unit> {
return try {
// Simulate email sending
if (!to.contains("@")) {
return Result.failure(AppError.ValidationError("Invalid email address: $to"))
}
if (kotlin.random.Random.nextDouble() < 0.3) {
throw Exception("SMTP server connection failed")
}
println("Email sent successfully: $to")
Result.success(Unit)
} catch (e: Exception) {
Result.failure(AppError.NetworkError("Email sending failed", e))
}
}
}
class UserService(
private val userRepository: UserRepository,
private val emailService: EmailService
) {
fun sendWelcomeEmail(userId: String): Result<Unit> {
return userRepository.findUser(userId)
.mapCatching { user ->
if (user.email.isBlank()) {
throw AppError.ValidationError("User email is empty")
}
user
}
.flatMap { user ->
emailService.sendEmail(
to = user.email,
subject = "Welcome!",
body = "Dear ${user.name}, welcome to our platform!"
)
}
}
fun processUserBatch(userIds: List<String>): Map<String, Result<Unit>> {
return userIds.associateWith { userId ->
sendWelcomeEmail(userId)
}
}
}
data class User(val id: String, val name: String, val email: String)
fun main() {
val userRepository = UserRepository()
val emailService = EmailService()
val userService = UserService(userRepository, emailService)
println("=== Error Propagation Example ===")
// Single user processing
listOf("1", "2", "999").forEach { userId ->
println("\nProcessing user: $userId")
userService.sendWelcomeEmail(userId).fold(
onSuccess = { println("Welcome email sent successfully") },
onFailure = { error ->
when (error) {
is AppError.ValidationError -> println("Validation error: ${error.message}")
is AppError.NetworkError -> println("Network error: ${error.message}")
is AppError.DatabaseError -> println("Database error: ${error.message}")
is AppError.BusinessLogicError -> println("Business logic error: ${error.message}")
else -> println("Unknown error: ${error.message}")
}
}
)
}
println("\n=== Batch Processing Example ===")
// Batch processing
val batchResults = userService.processUserBatch(listOf("1", "2", "999", "invalid"))
val successCount = batchResults.values.count { it.isSuccess }
val failureCount = batchResults.values.count { it.isFailure }
println("Batch processing results: $successCount successful, $failureCount failed")
batchResults.forEach { (userId, result) ->
val status = if (result.isSuccess) "✓" else "✗"
val message = result.fold(
onSuccess = { "Success" },
onFailure = { it.message }
)
println("$status User $userId: $message")
}
}Next Steps
After mastering exception handling, let's learn about file handling operations in Kotlin.
Next Chapter: File Handling
Exercises
- Create a network request library that uses Result type to handle various network errors
- Implement a config file parser with complete error handling and validation
- Design a batch data processing system that supports error recovery and retry mechanisms
- Write a logging system that safely handles file writing exceptions
- Create a user registration system with multi-layer validation and error handling