DEV Community

Bugfender
Bugfender

Posted on • Originally published at bugfender.com on

Mastering Exception Handling in Kotlin: A Comprehensive Guide

In programming, exception handling is an essential concept to help developers catch and manage errors in a way that will support the application’s architecture robustness. An Exception is an event which disrupts the normal flow of your Android application. It can occur if a user provides an invalid input, or there are runtime errors, such as division by zero.

Exceptions happen when there are unexpected conditions, like IOError file reading failed etc. In summary, it is the responsibility of developers to anticipate and avoid crashes using proper exception handling as much as possible; otherwise a (meaningful) error message should be displayed. That way, if an unexpected condition halts execution, at least this action does not occur silently.

Why is Exception Handling in Programming so important?

Exception handling is important for a number of reasons:

  • Program stability : Correctly managing exceptions can prevent applications from crashing and provide the end user with a better experience.
  • Diagnosing and errors: Creating efficient error messages and logging statements can allow for faster diagnosis of problems, so that you debug with ease and maintain your code.
  • Resource Management : Exceptions cause the release of resources (such as file handles or network connections) in a timely fashion, which would likely not happen using return codes.
  • Safety : By avoiding unhandled exception, developers can take control of transmitting sensitive data.

As a modern and concise programming language, Kotlin has excellent support for handling exceptions. Kotlin reduces the syntax and improves manageability, and is harmonious with Java. Kotlin Exception Handling is a very important part of the language, so here are some key points that you need to remember.

  • Try-Catch Block : For catching and handling exceptions Try Catch Minority for capturing Exception, if a list of lines has runtime error.
  • The finally block executes cleanup code, whether or not an exception was raised.
  • Custom Exceptions : We can define our own exceptions to deal with specific error scenarios.
  • Seamless integration with Coroutines : Kotlin’s coroutines framework has some neat facilities for exception handling in async code.

Understanding Exceptions

An exception is something that happens during the execution of a program, which takes up some time and causes it to execute differently. These are generally errors that cannot be handled by program, resulting in the termination of the application. An example would be trying to open a file that is not present with permissions, or trying to connect to a service that doesn’t exist. When an exception occurs, normal flow of the program is disrupted and control goes to a special block code called an “Exception Handler”.

Types of Exceptions in Kotlin

Kotlin and Java are interoperable, so Kotlin comes with the entire exception hierarchy of exceptions as Java. In Kotlin, some of the common types are:

i) RuntimeException: It is also an unchecked type of exception that happens during program execution.

NullPointerException : When you try to use an object reference that has not been initialized.

IndexOutOfBoundsException : This occurs when there is an attempt to access a valid index in the array or list, but this element doesn’t exists at that index.

IllegalArgumentException : Thrown to indicate that a method has presented an illegal or inappropriate argument.

ii) IOException: A checked exception that happens during I/O input/output operations and occurs when the user tries to open the pointed file, but its pathname has failed to access.

iii) SocketException: Signals that an error occurred when the socket was being created or accessed.

iv) SQLException: SQLException is a checked exception and gives information about database access errors or other errors.

v) ArithmeticException: Arithmetic exception to be thrown when an exceptional arithmetic condition has occurred.

Checked vs Unchecked Exceptions

Types of Exceptions in Kotlin: In Kotlin, the two most widely used types are classified as Checked Exception and Unchecked Exception. Java inheritance is the reason behind these two types, even though Kotlin does not enforce checked exceptions as heavily as Java.

Checked Exceptions

Such exceptions are called checked exceptions, and the compiler checks them at compile-time.

These exceptions are either caught using a try-catch block, or they appear in the method’s throws clause.

For example, IOException and SQLException.

fun readAFile(fileName: String) {
    try {
        val fileObj = File(fileName)
        val reader = BufferedReader(FileReader(fileObj))
        reader.use {
            println(it.readLine())
        }
    } catch (e: IOException) {
        println("An I/O error occurred: ${e.message}")
    }
}

Enter fullscreen mode Exit fullscreen mode

Unchecked Exceptions :

Unchecked Exceptions are not checked at compile-time. This means that, while the compiler will check whether these Unchecked exceptions have been handled or not, we don’t get a compilation error if they didn’t handle.

These are a checked exception and are not the subclasses of RuntimeException like NullPointerException, IndexOutOfBoundsException or IllegalArgumentException.

Most of the time, these exceptions indicate programming mistakes. For example, using a logic bug or using the wrong API.

fun divide(a: Int, b: Int): Int {
    return try {
        a / b
    } catch (e: ArithmeticException) {
        println("Cannot divide by zero: ${e.message}")
        0
    }
}

Enter fullscreen mode Exit fullscreen mode

Understanding how these concepts and common exceptions operate in the Kotlin world is a bare minimum to code robustly while ensuring future resistance. We will examine the real-world ways to handle these exceptions in upcoming sections.

Basic Exception Handling in Kotlin

Syntax of Kotlin try-catch Block

The try-catch block is used to handle exceptions in Kotlin. The basic syntax goes as follows:

try {
    // Code that may throw an exception
} catch (e: ExceptionType) {
    // Code to handle the exception
}

Enter fullscreen mode Exit fullscreen mode

The try block (containing the code which may produce exception) is executed, followed by catch block (contains logic to handle exception).

Basic Exception Handling Example

An example of exception handling using a Kotlin try catch block is shown below:

fun divide(a: Int, b: Int): Int {
    return try {
        a / b
    } catch (e: ArithmeticException) {
        println("Cannot divide by zero")
        0
    }
}

fun main() {
    val result = divide(10, 0)
    println(result) // Output: Cannot divide by zero
                    // 0
}

Enter fullscreen mode Exit fullscreen mode

Here, trying to divide by zero throws an ArithmeticException that is caught and given a custom error message with returning 0.

Multiple Catch Blocks

You can catch multiple type of exceptions by using different catch block in Kotlin. The multiple catch block is used for exception handling. Here, each and every catch block handles a specific type of exception.

fun readAFile(fileName: String) {
    try {
        val fileObj = File(fileName)
        val reader = BufferedReader(FileReader(fileObj))
        println(reader.readLine())
        reader.close()
    } catch (e: FileNotFoundException) {
        println("File not found: ${e.message}")
    } catch (e: IOException) {
        println("An I/O error occurred: ${e.message}")
    }
}
// main function
fun main() {
    readFile("nonexistent.txt")
    // Output: File not found: nonexistent.txt (No such file or directory)
}

Enter fullscreen mode Exit fullscreen mode

In this Kotlin function code, a FileNotFoundException is caught if the file cannot be located. Similarly, any other I/O error should be caught by IOException block.

Cleanup with finally Block

Finally – This is used to run code that we have written in it, regardless of whether the exception was thrown or caught. This is traditionally used for tear-down operations, such as releasing resources:

fun readFile(fileName: String) {
    var reader: BufferedReader? = null
    try {
        val file = File(fileName)
        reader = BufferedReader(FileReader(file))
        println(reader.readLine())
    } catch (e: FileNotFoundException) {
        println("File not found: ${e.message}")
    } catch (e: IOException) {
        println("An I/O error occurred: ${e.message}")
    } finally {
        reader?.close()
        println("Reader closed")
    }
}

fun main() {
    readFile("example.txt")
    // Output will vary depending on whether the file exists and whether there are I/O errors
}

Enter fullscreen mode Exit fullscreen mode

In the case of finally block, BufferedReader will be closed even in an exception.dequeue() here.

The try-catch block together with multiple catch blocks and the finally are all that you need to ensure your program is stable after exceptions have occurred and correct resource management requirements are met. This simple understanding will form the basis of Kotlin’s more advanced exception handling techniques.

Throwing Exceptions

Throwing Exceptions Syntax

You can do that using the throw keyword with an exception in Kotlin. Its syntax is simple –

throw ExceptionType("Exception message for user")

Enter fullscreen mode Exit fullscreen mode

When and Why to Throw Exceptions

We throw exceptions when we want to tell the process that an error has occurred and normal processing cannot be carried out. This is a fundamental requirement in the following cases:

  • Invalid Input: This is when a function receives an argument it can deal with.
  • The object is in an invalid state.
  • Resource Limitation : Resources exhaustion or unavailability in acceptable way (e.g files, network connections and memory).
  • If you break anything that has to be maintained for any of the above functions to work properly

Throwing exceptions is a good way to give the calling code an unambiguous signal that it has failed. Then, if possible, your caller can deal with this situation properly.

Here is a real-world sample that illustrates how to throw the built-in exceptions in Kotlin:

IllegalArgumentException : This exception should be thrown when a method passes an argument that it has no right to accept.

fun setAgeOfUser(age: Int) {
    if (age < 0) {
        throw IllegalArgumentException("Age cannot be negative")
    }
    println("Age is set to $age")
}

fun main() {
    try {
        setAgeOfUser(-5)
    } catch (e: IllegalArgumentException) {
        println(e.message) // Output: Age cannot be negative
    }
}

Enter fullscreen mode Exit fullscreen mode

IllegalStateException only when object is not in appropriate state

class BankAccount(private var balance: Double) {
    fun withdraw(amount: Double) {
        if (amount > balance) {
            throw IllegalStateException("Insufficient funds")
        }
        balance -= amount
        println("Withdrawn: $$amount, Remaining balance: $$balance")
    }
}

fun main() {
    val account = BankAccount(100.0)
    try {
        account.withdraw(150.0)
    } catch (e: IllegalStateException) {
        println(e.message) // Output: Insufficient funds
    }
}

Enter fullscreen mode Exit fullscreen mode

UnsupportedOperationException : Use this exception when you intend to not support an operation.

open class Vehicle {
    open fun start() {
        println("Vehicle started")
    }
}

class ElectricCar : Vehicle() {
    override fun start() {
        throw UnsupportedOperationException("ElectricCar cannot be started with this method")
    }
}

fun main() {
    val car: Vehicle = ElectricCar()
    try {
        car.start()
    } catch (e: UnsupportedOperationException) {
        println(e.message) // Output: ElectricCar cannot be started with this method
    }
}

Enter fullscreen mode Exit fullscreen mode

Custom Exception: Sometimes we create our own exception class to make message more specific and meaningful.

class NegativeBalanceException(message: String) : Exception(message)

class BankAccount(private var balance: Double) {
    fun deposit(amount: Double) {
        if (amount < 0) {
            throw NegativeBalanceException("Cannot deposit a negative amount")
        }
        balance += amount
        println("Deposited: $$amount, New balance: $$balance")
    }
}

fun main() {
    val account = BankAccount(100.0)
    try {
        account.deposit(-50.0)
    } catch (e: NegativeBalanceException) {
        println(e.message) // Output: Cannot deposit a negative amount
    }
}

Enter fullscreen mode Exit fullscreen mode

Once you get the hang of where and how exceptions can be thrown, it will make your Kotlin code cleaner and more maintained. Using exceptions appropriately makes sure that your errors are fatal and the program is able to recover, or inform that an error has occurred, and as a programmer you will be well prepared for it.

Best Practices for Exception Handling

Catch Specific Exceptions

Different exceptions should be handled differently. That way, catching them and dealing with their specific case is more manageable to debug, since the error message also specified what went wrong. By implementing catching-specific exception practice, you can write a special response for every error condition.

fun readMyFile(fileName: String) {
    try {
        val fileObj = File(fileName)
        val reader = BufferedReader(FileReader(fileObj))
        println(reader.readLine())
        reader.close()
    } catch (e: FileNotFoundException) {
        println("File not found: ${e.message}")
    } catch (e: IOException) {
        println("An I/O error occurred: ${e.message}")
    }
}

Enter fullscreen mode Exit fullscreen mode

Avoid Silent Failures

A silent failure is when an exception is caught and not handled or logged properly, so you may never learn that something has gone wrong. Exceptions should not be caught only to throw the same exception (or re-wrap an existing one) without any other action or logging in place, as it makes troubleshooting harder.

try {
    // Some code that may throw an exception
} catch (e: Exception) {
    // Do nothing
}

Enter fullscreen mode Exit fullscreen mode

Proper Handling:

try {
    // Some code that may throw an exception
} catch (e: Exception) {
    println("An error occurred: ${e.message}")
}

Enter fullscreen mode Exit fullscreen mode

Use finally for Resource Cleanup

Finally : The finally block is a code that will always be executed whether an exception was thrown or not. This is helpful with clean-up resources e.g. closing file and network connection

fun readMyFile(fileName: String) {
    var reader: BufferedReader? = null
    try {
        val fileObj = File(fileName)
        reader = BufferedReader(FileReader(fileObj))
        println(reader.readLine())
    } catch (e: IOException) {
        println("An error occurred: ${e.message}")
    } finally {
        reader?.close()
        println("Reader closed")
    }
}

Enter fullscreen mode Exit fullscreen mode

Throw Meaningful Exceptions

When throwing exceptions, exception messages should describe the issue. It will help others to gain a clear idea of what happened and how to resolve it.

fun setAge(age: Int) {
    if (age < 0) {
        throw IllegalArgumentException("Age cannot be negative")
    }
    println("Age is set to $age")
}

fun main() {
    try {
        setAge(-5)
    } catch (e: IllegalArgumentException) {
        println(e.message) // Output: Age cannot be negative
    }
}

Enter fullscreen mode Exit fullscreen mode

Logging Exceptions

We should log/record the exceptions so that when something goes wrong, we can better understand what actually happened. Log exception details, including stack trace, help you see and track the error. You can use any logging framework for this.

import java.util.logging.Logger

val logger = Logger.getLogger("Logger")

fun readFile(fileName: String) {
    try {
        val file = File(fileName)
        val reader = BufferedReader(FileReader(file))
        println(reader.readLine())
        reader.close()
    } catch (e: IOException) {
        logger.severe("An I/O err has occurred: ${e.message}")
    }
}

fun main() {
    readFile("exampleFile.txt")
    // Check the log for error information
}

Enter fullscreen mode Exit fullscreen mode

Using a Crash Reporting tool

Even with well-written code and extensive exception handling, unexpected situations may still cause the application to crash. A crash reporting tool can be extremely helpful by providing insights into the cause of crashes from uncaught exceptions and revealing overlooked scenarios. Tools like Bugfender are valuable additions to any development toolkit and are among the easiest ways to improve error handling.

Bugfender stands out because it handles both crash reporting and logging. Rather than storing logs on the user’s device, as in the previous example, Bugfender enables you to view logs in real time on its dashboard.

You can try Bugfender for free—just create an account and start enhancing your app’s quality.

Once you install the Bugfender SDK in your app, it will capture any runtime exceptions and send all relevant information to the dashboard. This lets you review crash data along with logs and user interactions leading up to the crash, giving you valuable insights into how to improve your app’s stability.

Benefits of Exception Handling in Kotlin

  1. Checked exceptions : Checked exceptions in Java can be a very painful experience. since the compiler forces you to handle the real deal. It is made easy on Kotlin side (no compile error), giving easier handling and better result, and we have code base that is cleaner, with improved readable and better maintainability.
  2. Improved Readability : The syntax of Kotlin makes code easier to read.
  3. Extension Functions : Kotlin has many great extension functions, like use, to simplify the resource management.
  4. Interoperability : Kotlin’s is directly interoperable with Java, making it simpler for developers to use the existing Java libraries and frameworks while coding using Kotlin.

Conclusion

In this article, we discussed the key points of exception handling in Kotlin. Exception Handling basic concepts of try-catch blocks, multiple catch and finally block for resource cleanup.

Throwing exceptions – including when to throw an exception and why, with example scenarios of throwing inbuilt exceptions. Exception Handling, Catching specific exceptions, avoiding silent failures, using the finally block, throwing meaningful exceptions and logging exception.

Exception handling is one of the key factors for building a strong, maintainable and user-oriented application. They ensure we handle the exceptions correctly, so that mobile applications can recover more gracefully from errors, avoid crashes and make for a better user experience.

We also discussed better logging and the showing of more useful error messages, so developers are able to troubleshoot issues in no time. This better helps maintain resources, so that memory leaks are prevented and applications work smoothly. The same is true for writing quality Kotlin code, which is why you must incorporate best practices in this area.

You will be able to do this by using methods and strategies that are talked about here in the system, thus making sure errors are handled correctly and your applications return quality feedback to users.

That’s it for the Kotlin exception-handling. Happy Coding!

Top comments (0)