Kotlin Exception Handling Comprehensive Guide Try Catch Finally Best Practice

Kotlin Exception Handling

How to do exception handling in Kotlin?

In programming, sometimes unexpected things can happen, like trying to divide a number by zero or reading a file that doesn’t exist. Exception handling is like having a safety net to catch these unexpected situations. In Kotlin, we use try, catch, and finally to handle these situations.

fun divide(a: Int, b: Int): Int {
    return try {
        // Code that might cause an issue (e.g., division by zero)
        a / b
    } catch (e: ArithmeticException) {
        // Code to handle the issue (e.g., print an error message)
        -1
    } finally {
        // Code that always runs, whether there's an issue or not
    }
}

fun main() {
    // Calling the function
    val result = divide(10, 2)
    // Printing the result
    println("Result: $result")
}
Kotlin

Explanation:

  • try block: It contains the code that might cause an issue, like dividing by zero (a / b).
  • catch block: If an issue occurs, this block is executed. It contains code to handle the issue, such as printing an error message (-1 is returned as an example).
  • finally block: This block always runs, whether there’s an issue or not. It is used for code that should execute regardless of what happens.

Examples of Simple Exception Handling

Now, let’s see another example where we ask the user for input and handle the case when the input is not a number.

fun getUserInput(): Int {
    return try {
        // Attempting to read a number from the user
        print("Enter a number: ")
        readLine()!!.toInt()
    } catch (e: NumberFormatException) {
        // Handling the case when the input is not a number
        println("Error: Not a valid number!")
        
    }
}

fun main() {
    // Calling the function to get user input
    val userInput = getUserInput()
    // Printing the user's input or a default value in case of an error
    println("User entered: $userInput")
}
Kotlin

Explanation:

  • The try block attempts to read a number from the user using readLine()!!.toInt().
  • If the input is not a number (causing a NumberFormatException), the catch block is executed. It prints an error message and returns a default value (0 in this case).
  • In the main function, we call the getUserInput function, and the user’s input (or the default value in case of an error) is printed.

This is a simplified way of handling unexpected situations in your code. The try block contains the risky code, the catch block deals with issues, and the finally block ensures that some code always runs, providing a safety net for your program.

In Kotlin, exceptions are broadly categorized into two types: Checked and Unchecked Exceptions.


Checked Exceptions:

  • These are exceptions that the compiler checks you to handle explicitly.
  • They are usually related to external factors beyond the program’s control, such as file I/O operations or network connections.
  • You must either catch these exceptions using a catch block or declare that your function/method throws these exceptions using the throws clause.
fun readFile() {
    try {
        // Code that might throw a checked exception
    } catch (e: IOException) {
        // Handle the IOException
    }
}
Kotlin

Unchecked Exceptions:

  • These are exceptions that the compiler does not force you to handle explicitly.
  • They typically represent programming errors and are subclasses of RuntimeException.
  • Unchecked exceptions are not anticipated during the compilation process, and the compiler doesn’t enforce handling them.
fun divide(a: Int, b: Int): Int {
    // Code that might throw an unchecked exception (e.g., ArithmeticException)
    return a / b
}
Kotlin
2. Throwable Hierarchy

The Throwable class is at the top of the exception hierarchy in Kotlin. It has two main subclasses: Error and Exception.

  • Error:
    • Represents serious issues that are usually beyond the control of the program.
    • Errors are not meant to be caught or handled by regular application code.
    • Examples include OutOfMemoryError and StackOverflowError.
  • Exception:
    • Represents issues that can be anticipated and handled by your program.
    • Exceptions are further divided into Checked and Unchecked Exceptions.
    • Examples include IOException, NullPointerException, and ArithmeticException.

Throwable Hierarchy:

Throwable
├── Error
│   ├── OutOfMemoryError
│   └── StackOverflowError
└── Exception
    ├── RuntimeException (Unchecked)
    │   ├── ArithmeticException
    │   ├── NullPointerException
    │   └── ...
    └── Other Exceptions (Checked)
        ├── IOException
        ├── SQLException
        └── ...
Kotlin

Understanding the Throwable hierarchy helps you categorize and handle exceptions effectively in your Kotlin code. It allows you to differentiate between critical errors that might require special handling (Error) and exceptions that can be managed within your application logic (Exception)

  • RuntimeException (Unchecked): These are unexpected issues that might occur while the robot is doing its thing but aren’t explicitly planned for.
    • ArithmeticException: If the robot tries to divide by zero, it faces an arithmetic problem.
    • NullPointerException: If the robot is asked to find something that doesn’t exist, it’s like asking it to look in an empty box.
  • Other Exceptions (Checked): These are issues that we know might happen, and we plan for them in advance.
    • IOException: Think of the robot working with external devices (like reading from a USB drive) and the drive suddenly disconnects.
    • SQLException: If the robot is dealing with a database and can’t find the information it needs.

Creating Custom Exception Classes

In Kotlin, you can create your own custom exceptions by defining classes that inherit from the Exception class or its subclasses. This allows you to represent specific error scenarios in your application.

In our banking application, we can define custom exceptions to represent various scenarios:

// Custom exception for insufficient funds
class InsufficientFundsException(accountNumber: String, requiredAmount: Double, availableBalance: Double) :
    Exception("Account $accountNumber does not have sufficient funds. Required: $requiredAmount, Available: $availableBalance")

// Custom exception for invalid account number
class InvalidAccountNumberException(accountNumber: String) :
    RuntimeException("Invalid account number: $accountNumber")

// Custom exception for transaction limit exceeded
class TransactionLimitExceededException(limit: Double) :
    RuntimeException("Transaction limit exceeded. Maximum allowed: $limit")
Kotlin

In the above example:

  • InsufficientFundsException represents a scenario where an account does not have enough funds for a transaction.
  • InvalidAccountNumberException represents an invalid account number scenario.
  • TransactionLimitExceededException represents a case where a transaction exceeds a predefined limit.

Throwing and Handling Custom Exceptions

Now, let’s use these custom exceptions in a banking service:

class BankingService {

    fun performTransaction(accountNumber: String, amount: Double) {
        try {
            validateAccountNumber(accountNumber)
            validateTransactionLimit(amount)

            // Perform the transaction logic
            // (For simplicity, we assume the transaction is successful in this example)
            println("Transaction successful for account $accountNumber. Amount: $amount")
        } catch (e: InsufficientFundsException) {
            // Handle insufficient funds scenario
            println("Transaction failed: ${e.message}")
        } catch (e: InvalidAccountNumberException) {
            // Handle invalid account number scenario
            println("Transaction failed: ${e.message}")
        } catch (e: TransactionLimitExceededException) {
            // Handle transaction limit exceeded scenario
            println("Transaction failed: ${e.message}")
        }
    }

    private fun validateAccountNumber(accountNumber: String) {
        if (accountNumber.length != 10) {
            throw InvalidAccountNumberException(accountNumber)
        }
        // Additional validation logic
    }

    private fun validateTransactionLimit(amount: Double) {
        val transactionLimit = 1000.0
        if (amount > transactionLimit) {
            throw TransactionLimitExceededException(transactionLimit)
        }
        // Additional validation logic
    }
}

fun main() {
    // Example usage of the banking service
    val bankingService = BankingService()
    bankingService.performTransaction("1234567890", 500.0)  // Valid transaction
    bankingService.performTransaction("9876543210", 1500.0) // Exceeds transaction limit
}
Kotlin

In this example:

  • The BankingService class has a method performTransaction that performs a banking transaction.
  • Custom exceptions are thrown based on different scenarios such as insufficient funds, invalid account number, or exceeding the transaction limit.
  • The main function demonstrates the usage of the BankingService with two different transactions, one valid and one exceeding the transaction limit.

This example provides a more realistic scenario where custom exceptions are created to handle specific issues that may arise in a banking application. The try-catch blocks in the performTransaction method handle these custom exceptions gracefully.

Kotlin Exception Handling in File I/O

Note:

“I/O” stands for Input/Output. In the context of computer systems and programming, I/O refers to the communication and data transfer between a program and external devices, such as storage devices, network devices, and user input/output devices.

Kotlin provides a rich set of functions and classes for working with files. When dealing with File I/O, exceptions may occur due to various reasons such as file not found, permission issues, or I/O errors. Here’s an overview of how you can handle exceptions in File I/O:

import java.io.BufferedReader
import java.io.FileReader
import java.io.IOException

fun readFile(filename: String) {
    try {
        val reader = BufferedReader(FileReader(filename))
        var line: String?
        
        while (reader.readLine().also { line = it } != null) {
            // Process each line of the file
            println(line)
        }
    } catch (e: IOException) {
        // Handle file I/O exception
        println("Error reading file: ${e.message}")
    }
}
Kotlin

In this example:

  • The try block contains the code that might throw exceptions during file reading.
  • The catch block catches an IOException (a common exception for I/O operations) and handles it appropriately.
  • You can log the exception, print an error message, or take other corrective actions.

Kotlin Network Operations and Exception Handling

Network operations, such as making HTTP requests, can result in exceptions due to connectivity issues, timeouts, or other network-related problems. Here’s an example of exception handling in network operations:

import java.net.URL
import java.io.IOException

fun fetchDataFromUrl(urlString: String) {
    try {
        val url = URL(urlString)
        val connection = url.openConnection()

        // Perform network operations
        // (Implementation details depend on the specific use case)
    } catch (e: IOException) {
        // Handle network-related exception
        println("Error fetching data: ${e.message}")
    }
}
Kotlin

In this case:

  • The try block contains the code for establishing a connection and performing network operations.
  • The catch block catches an IOException and handles it, which can include logging the error or displaying a user-friendly message.

Kotlin Database Interactions Exception Handling

When interacting with databases, exceptions may occur due to issues like connection problems, SQL errors, or database server unavailability. Here’s an example of exception handling in database interactions:

import java.sql.DriverManager
import java.sql.SQLException

fun performDatabaseOperation() {
    try {
        val connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "user", "password")

        // Perform database operations
        // (Implementation details depend on the specific use case)
    } catch (e: SQLException) {
        // Handle database-related exception
        println("Database error: ${e.message}")
    }
}
Kotlin

How do Coroutines handle exceptions in Kotlin?

Understanding Coroutines

Kotlin Coroutines have revolutionized asynchronous programming, offering a concise and expressive way to write asynchronous code. We’ll focus on an essential aspect of robust asynchronous programming: Exception Handling. Buckle up as we explore how to handle errors gracefully and maintain structured concurrency in your coroutine-based applications.

Structured Concurrency

One of the key strengths of Kotlin Coroutines is structured concurrency. Unlike traditional threading models, structured concurrency ensures that coroutines are tied to a specific scope, making it easier to manage their lifecycle and handle exceptions. Let’s illustrate this concept with a simple example:

import kotlinx.coroutines.*

fun main() = runBlocking {
    try {
        launch {
            // Coroutine 1
            delay(100)
            println("Task 1 completed")
        }

        launch {
            // Coroutine 2
            delay(50)
            throw IllegalArgumentException("Oops! Something went wrong in Task 2")
        }
    } catch (e: Exception) {
        // Handle exceptions here
        println("Caught an exception: ${e.message}")
    }
}
Kotlin

Explanation:

  • runBlocking: This function creates a new coroutine and blocks the current thread until its completion. It’s often used in main functions to launch coroutines.
  • launch: Launches a new coroutine in the specified scope. In this example, two coroutines are launched concurrently.
  • delay: Suspends the coroutine for a specified time. Used here to simulate tasks taking different durations.
  • throw IllegalArgumentException: Simulates an exception occurring in one of the coroutines.
  • catch (e: Exception): Catches any exceptions thrown within the runBlocking scope.

Asynchronous Operations

Now, let’s explore combining asynchronous operations using async and await. This allows you to run multiple asynchronous tasks concurrently and await their results. Here’s an example:

import kotlinx.coroutines.*

suspend fun fetchDataAsync(): String {
    delay(100)
    return "Fetched data"
}

suspend fun processAsyncData(data: String): String {
    delay(50)
    return "Processed: $data"
}

fun main() = runBlocking {
    try {
        val result = coroutineScope {
            val dataJob = async { fetchDataAsync() }
            val processedDataJob = async { processAsyncData(dataJob.await()) }

            processedDataJob.await()
        }

        println("Result: $result")
    } catch (e: Exception) {
        println("Caught an exception: ${e.message}")
    }
}
Kotlin

Explanation:

  • async: Creates a coroutine that performs asynchronous operations. It returns a Deferred object, allowing you to asynchronously retrieve the result.
  • await: Suspends the coroutine until the result is ready, allowing you to combine multiple asynchronous operations.

Kotlin Functions

Advanced Functions Higher-Order Functions – Lambda Functions – Extension Functions – Infix Functions- Generic Functions
See More Info

Functions– Simple Functions – Function Parameters – Function Return Types – Function Scope
See More Info

Functional Programming FunctionsImmutability and Pure Functions- Function Composition – Functional Operators
See More Info

Feel free to follow and join my email list at no cost. Don’t miss out — sign up now!

Please check out my earlier blog post for more insights. Happy reading!

The Innovative Fundamentals of Android App Development in 2023

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x