Skip to content

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

  1. Create a network request library that uses Result type to handle various network errors
  2. Implement a config file parser with complete error handling and validation
  3. Design a batch data processing system that supports error recovery and retry mechanisms
  4. Write a logging system that safely handles file writing exceptions
  5. Create a user registration system with multi-layer validation and error handling

Content is for learning and research only.