Kotlin Training Program

DOWNLOAD APP

FEEDBACK

Exceptions

While programming we may encounter two types of errors :

Compile time errors

These are the syntax errors that prevent our code from compiling. We can’t execute the code that contains compile time errors.

Examples :

 fun main() {
		val x: Int = 3.5f // Float can't be assigned to Int variable
		val y = 3 * "ABC" // Invalid operation
		val z: String = null // null can't be assigned to a Non-null variable
}
 

Run time errors

These are the errors (aka Exceptions) that are thrown (occur) during the execution of a program. If not handled, they make the program crash.

Crash refers to abrupt termination of a program.

Program crash leads to bad user experience and may even cause data loss and data inconsistency. Run time errors (aka Exceptions) need to be handled in order to avoid the program from crashing.

Basics

Examples

Following are some examples of exceptions (Run time errors) :

ArithmeticException

Consider the following program that takes two int inputs from user and prints the quotient of their division :

 fun main() {
    val a = inputInt()
    val b = inputInt()
    println("a / b = ${a / b}")
}

/* Output :

Enter an integer : 10
Enter an integer : 2
a / b = 5
 */
 

An ArithmeticException is thrown when we enter the second operand as zero :

 Enter an integer : 10
Enter an integer : 0
Exception in thread "main" java.lang.ArithmeticException: / by zero
		at MainKt.main(Main.kt:4)
 

Division by zero is not supported so the program crashes while executing line#4. No code is executed after the crash.

IndexOutOfBoundsException

This exception is thrown when trying to access element at a index which is greater than the size of String / List :

 fun main() {
    print(
        "Hello"[5]
    )
}

/* Output :

Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 5
	at java.base/java.lang.StringLatin1.charAt(StringLatin1.java:47)
	at java.base/java.lang.String.charAt(String.java:693)
	at MainKt.main(Main.kt:3)
 */
 

Hello is a string of length 5 supporting indices in the range 0…4, but we are trying to access its 5th index which does not exist. So, an exception is thrown.

NullPointerException (NPE)

This is the most common exception in Java because it allows all variables to hold null :

 class Main {
    public static void main(String[] args) {
        String x = null; // Allowed
				x.trim(); // Will cause NPE
    }
}
 

This is not possible in Kotlin because Kotlin has two types of variables - Non-null & Nullable (declared using ?). Only nullable variables can hold null value.

The NullPointerException is thrown when invoking a function on null values. This can never occur for Non-null variable because they can never contain null value. However for Nullable variables, this can happen if we try to access member of a nullable object using !! operator. Example :

 fun main() {
    val x: String? = null
    print(x!!.length)
}

/* Output :

Exception in thread "main" java.lang.NullPointerException
	at MainKt.main(Main.kt:3)
 */
 

Here we are trying to access length property of a String nullable object which is actually null but we are asserting it to be not-null. This throws a NullPointerException.

NullPointerException can be prevented using safe call operator :

 fun main() {
    val x: String? = null
    print(x?.length) // No exception is thrown
}

/* Output :

null
 */
 

Default Behaviour

When an exception occurs, the program crashes. To help us know what went wrong, the exception name, message and stacktrace is printed :

 Exception in thread /* threadName */ /* ExceptionName */ : /* ExceptionMessage */
		at /* FileName, FunctionName, LineNum where exception occurred */
		at /* Location of invocation of above function */
		at /* Location of invocation of above function */
		at ...
 

Stacktrace refers to the order of execution of functions that led to an exception.

Consider the following order of execution of functions : a() → b() → c() i.e. a() invokes b() and b() invokes c() :

 a() { b() }
b() { c() }
c() { }
 

If exception occurs in c() then stacktrace would show c() at the top, then b() and at last a() :

 Exception in thread /* ... */
		at c(...)
		at b(...)
		at a(...)
 

Stacktrace helps us get to the root of the exception and fix it.

Using try-catch block, we can override this defualt behaviour in which program crashes and stacktrace is printed.

Exception Handling

We can prevent the program from crash by handling exceptions using the try-catch block :

 try {
		// Code that can throw exception
} catch (e: /* Exception to catch */) {
		// Handle exception
}
 

When an exception occurs in the try block, the code inside catch block is executed instead of a crash.

Basic example

 fun main() {
    val a = inputInt()
    val b = inputInt()

    try {
        println("a / b = ${a / b}")
    } catch (e: ArithmeticException) {
        println("Division by zero is not supported!")
    }
}

/* Output :

Enter an integer : 5
Enter an integer : 0
Division by zero is not supported!
 */
 

Multiple exceptions

Multiple exceptions can be handled by defining multiple catch blocks :

 try {
		// Code that can throw exception
} catch (e: /* Exception to catch */) {
		// Handle exception
} catch (e: /* Another exception to catch */) {
		// Handle exception
} catch (e: /* Yet another exception to catch */) {
		// Handle exception
}
 

At least one catch block must be defined.

Example :

 fun main() {
    try {
        print("Enter a : ")
        val a = readln().toInt()

        print("Enter b : ")
        val b = readln().toInt()

        println("a / b = ${a / b}")
    } catch (e: NumberFormatException) {
				// Catches NumberFormatException that may occur in toInt()
        println("Invalid input (${e.message})!")
    } catch (e: ArithmeticException) {
				// Catches NumberFormatException that may occur while division
        println("Division by zero is not supported!")
    }
}

/* Output 1 :

Enter a : A
Invalid input (For input string: "A")!
 */

/* Output 2 :

Enter a : 4
Enter b : 0
Division by zero is not supported!
 */
 

Note that we can access exception params like name, message & stacktrace using the exception object received in catch block :

 println("Invalid input (${e.message})!") // Prints the exception message also
 

General catch block

All exceptions inherit from the Exception class. If we add a catch block with Exception class, it will catch all possible exceptions :

 try {
		// Code that can throw exception
} catch (e: Exception) {
		// Catches all kinds of exceptions
}
 

Example :

 fun main() {
    try {
        print("Enter a : ")
        val a = readln().toInt()

        print("Enter b : ")
        val b = readln().toInt()

        println("a / b = ${a / b}")
    } catch (e: Exception) {
        println("ERROR - (${e.message})!")
    }
}

/* Output 1 :

Enter a : A
ERROR - (For input string: "A")!
 */

/* Output 2 :

Enter a : 4
Enter b : 0
ERROR - (/ by zero)!
 */
 

Note that only single catch block is executed, following the order they are defined in. If we define multiple catch blocks for an exception, only the first that matches, will be executed :

 try {
		// ...
} catch (e: NumberFormatException) {
		println("Invalid input (${e.message})!")
} catch (e: Throwable) {
		println("ERROR - (${e.message})!")
}

/* Output :

Enter a : 4
Enter b : 0
ERROR - (/ by zero)!
 */
 

Even though both catch blocks can catch NumberFormatException, only the first one is executed.

finally block

We can optionally define a finally block which gets executed after try-catch irrespective of whether exception occurred.

 try {
		// Code that can throw exception
} catch (e: /* Exception to catch */) {
		// Handle exception
} catch (e: /* Another exception to catch */) {
		// Handle exception
} finally {
		// Code to be executed after try-catch
}
 

Example :

 fun main() {
    divisionCalculator()
}

fun divisionCalculator() {
    try {
        print("\nEnter a : ")
        val a = readln().toInt()

        print("Enter b : ")
        val b = readln().toInt()

        println("a / b = ${a / b} \n")
    } catch (e: Exception) {
        println("ERROR - (${e.message})! \n")
    } finally {
        print("Retry? (Y/N) : ")
        val input = readln()
        if (input == "Y") {
            divisionCalculator()
        }
    }
}

/* Output :

Enter a : 120
Enter b : 5
a / b = 24

Retry? (Y/N) : Y

Enter a : 100
Enter b : 0
ERROR - (/ by zero)!

Retry? (Y/N) : Y

Enter a : A
ERROR - (For input string: "A")!

Retry? (Y/N) : N
 */
 

Note :

  • We have defined a new function divisionCalculator() that prompts the user to input two ints and ouputs quotient of their division. Any error caught is handled by printing the error message.
  • We have used the finally block to prompt the user for a retry. Irrespective of exception, this will always be shown.

Using StdLib functions

We can handle some exceptions simply by using StdLib functions and no try-catch blocks :

  • String # toIntOrNull() instead of toInt() : Prevents NumberFormatException
  • String / List # getOrNull() instead of get() or [] : Prevents IndexOutOfBoundsException

These functions are discussed in detail under Null section.

The best way

The best way to handle exceptions in an application is using Coroutine Exception Handler. We’ll dicuss about this in the Coroutines Module.

Throwing Exceptions

Apart from exceptions thrown by library functions, we can also throw exceptions as and when we need. For this, we have a built-in exception - IllegalStateException. IllegalStateException indicates an error that something is not as expected.

To throw an exception, we use the throw keyword followed by an instance of Exception class or its subclass :

 throw /* Execption / Throwable / Subclass name */()

// Examples :
throw Exception("Invalid file name!")
throw IllegalStateException("Positive number required!")
 

Example

Consider the following program that multiplies a string i.e. repeats a string certain number of times and returns it. Eg. - “Ha” * 3 = “HaHaHa”

 fun multiplyText(text: String, noOfTimes: Int): String {
    var result = ""
    repeat(noOfTimes) { result += text }
    return result
}

fun main() {
    println(
        multiplyText("Ha", 3)
    ) // Prints "HaHaHa"
}
 

Technically this function should not accept negative and zero value for the argument noOfTimes. But it simply returns empty string in that case :

 println(
		multiplyText("Ha", -2)
) // Prints ""
 

We can throw an exception for this case. Because an illegal (invalid) argument is passed, we can throw the built-in IllegalArgumentException :

 fun multiplyText(text: String, noOfTimes: Int): String {
    if (noOfTimes <= 0) {
        throw IllegalArgumentException("noOfTimes must be greater than 0, found $noOfTimes!")
    }

    var result = ""
    repeat(noOfTimes) { result += text }
    return result
}

fun main() {
    println(
        multiplyText("Ha", -2)
    ) // Throws Exception
}

/* Output :

Exception in thread "main" java.lang.IllegalArgumentException: noOfTimes must be greater than 0, found -2!
	at MainKt.multiplyText(Main.kt:3)
	at MainKt.main(Main.kt:13)
 */
 

error() & Nothing

We also have a short-cut to throw IllegalStateException :

 error(/* Message */)

// instead of 

throw IllegalStateException(/* Message */)
 

error() is built-in function that wraps the throw statement :

 fun error(message: Any): Nothing = throw IllegalStateException(message.toString())
 

Notice the return type of this function - Nothing.

Nothing is a special class which is used to specify the return type of functions that never return i.e. always throw an exception.

We can define our own Nothing returning function to throw exceptions :

 fun illegalArgumentError(
    argumentName: String,
    expected: Any,
    actual: Any
): Nothing {
    throw IllegalArgumentException("Illegal $argumentName: expected: $expected, actual: $actual")
}

// Usage :
fun multiplyText(text: String, noOfTimes: Int): String {
    if (noOfTimes <= 0) {
        illegalArgumentError(
            argumentName = "noOfTimes",
            expected = "greater than 0",
            actual = noOfTimes
        )
    }

    var result = ""
    repeat(noOfTimes) { result += text }
    return result
}
 

multiplyText() function immediately returns when illegalArgumentError() function is invoked because it throws an exception.

Custom Exceptions

To define our own exceptions, we can extend (inherit) from the Exception class. Exception class has a field message which denotes the error message. It is optional to pass this to the constructor while inheriting :

 // With message
class /* ExceptionName */(
		message: String
): Exception(message)

// Without message
class /* ExceptionName */: Exception()

// Example :
class NotExistsException(
		what: String
): Exception("$what does not exist")
 

TODO()

Kotlin provides TODO() function which we can use as a placeholder in code or function which is yet to be implemented.

Suppose we are working on a project that involves three functions :

 fun load(name: String): String
fun store(name: String, content: String): Boolean
fun count(): Int
 

We declared these functions at once but will define them functions one by one. After defining the load() function, we want to run the program to test it :

 fun load(name: String): String {
		// Code ...
}

fun store(name: String, content: String): Boolean { }

fun count(): Int { }
 

Problem : The other two functions are yet to be defined and they have a return type so compiler shows the following error and we can’t run our code :

 A 'return' expression required in a function with a block body ('{...}')
 

We have two options, either we can return a dummy value or comment out the function for now :

 // Return Dummy value
fun store(name: String, content: String): Boolean = false

// Commented out
// fun count(): Int { }
 

This works, but there is a better way to do the same. We can use the TODO() function as a placeholder :

 fun store(name: String, content: String): Boolean = TODO()

fun count(): Int = TODO()
 

This hides the error and we can run our code successfully. Note that we need not write any return statement and it works for any return type!

Once we have implemented the missing code, we can simply replace the TODO() function with the actual implementation.

If we invoke a function which is defined as TODO(), a NotImplementedError exception is thrown :

 fun count(): Int = TODO()

fun main() {
    print(count())
}

/* Output :

Exception in thread "main" kotlin.NotImplementedError: An operation is not implemented.
	at MainKt.count(Temp.kt:1)
	at MainKt.main(Temp.kt:4)
 */
 

This is good because if we would have defined a dummy value, we couldn’t know that the function is yet to be implemented. But using TODO(), this error reminds us to implement the function before use.

Optionally, we can provide a reason String to the TODO() function :

 TODO(/* reason */)

// Example :
fun count(): Int = TODO("Waiting for version upgrade")
 

TODO() function always throws an error, so it has Nothing as return type :

 fun TODO(): Nothing = throw NotImplementedError()

fun TODO(reason: String): Nothing = throw NotImplementedError("An operation is not implemented: $reason")
 

Hierarchy

Recall that all exceptions inherit from the Exception class. Further, Error and Exception class inherit from Throwable class. Throwable class is the base class of all Errors and Exceptions.

Following is a soft (generally speaking) comparisons between the two types of Throwable - Error & Exception :

Error Exception
End user relevance No, meant for Developer only Maybe, depends on the requirement
Should be caught using try-catch No Maybe, depends on the requirement
Examples AssertionError, NotImplementedError ArithmeticException, IndexOutOfBoundsException, IOException

It is better not to catch Error like NotImplementedError (thrown by TODO()) because it is a development error and end user can’t do anything about it. It is okay to let the program crash in such scenario.

In catch block, if we use Throwable instead of Exception then Error & its subclasses will also be caught :

 try { 
		//...
} catch (e: Throwable) {  // Instead of catch (e: Exception)
		//...
}
 

It will catch Error like NotImplementedError also which we may not want. So, we generally use Exception class only - for catching all exceptions and defining custom exceptions :

 try { 
		//...
} catch (e: Exception) {
		//...
}

// Defining custom exceptions
class /* ExceptionName */(): Exception()
 

Alternatively, we can also extend Error or Throwable class to define custom exceptions.

 class /* ExceptionName */: Error()

class /* ExceptionName */: Throwable()
 

But use it considering the following point :

  • catch block accepting Exception instance will not be able to catch Error or Throwable instance / subclass. catch block accepting Error or Throwable instance need to be used instead :

     try { 
    		//...
    } catch (e: Error /* or Throwable */) {
    		//...
    }
     

    When accepting Error, Exception instance / subclass won’t be caught.

As a general rule, you can :

  • Use catch block accepting Exception and not Error or Throwable.
  • Extend Exception for custom exceptions that are meant to be caught.
  • Extend Error for custom exceptions that are not meant to be caught.