How to: Nulls and Exceptions in Kotlin

~ 5 minutes read
no time? jump straight to the conclusion

Kotlin’s type system is aimed at eliminating the danger of null references from code […]

Kotlin’s type system is aimed to eliminate NullPointerException’s from our code.

https://kotlinlang.org/docs/reference/null-safety.html

The possible causes of Null Pointer Exceptions (NPEs) in Koltin include Java interoperation. Recently I worked with Kotlin and had to use a Java library that throw exceptions.

This write up is about approaches how to deal with nulls and exceptions in kotlin.

The following approaches and related topics looked promising to me:

There are small code blocks to highlight the approaches below. Parse and reciprocal functions are referenced because there are short and many engineers know these concepts.

Exception Handling

Kotlin offers try / catch similar to Java. Try is also an expression and can have a return value. Also there are no checked exceptions. Although slightly improved compared to java, exception handling was not an option for me to invest into.

fun reciprocal(i: Int): Double =
    when(i == 0){
        true -> throw IllegalArgumentException(
            "Can not take reciprocal of 0."
        )
        false -> 1.0 / i
    }
@Test(expected = IllegalArgumentException::class)
fun testReciprocalException() {
    ExceptionStyle.reciprocal(ZERO_INT)
}

gist: exception handling


The following approaches aim to model null and exceptions as types within the Kotlin type system

3 special Kotlin Types

To take full advantage of Kotlin Type Hierarchy is essential to understand how these three special types do work.

Unit, Nothing, Any (and null)

Nothing

Kotlin’s Nothing type does not let you pass a value as return type, however it allows to throw an exception. Catching an exception isn’t improved compared to the exception handling approach.

fun parse(): Nothing = throw IllegalArgumentException(
    "Can not parse anything."
)
@Test(expected = IllegalArgumentException::class)
fun testParse(){
    NothingStyle.parse()
}

gist: nothing

Unit

The Unit type is the corresponding Kotlin type to the void type in Java. While completely different than null, it represents nothing without causing an NPE. Unit return type is useful to produce side effects in functions. However, unit did not help me dealing with nulls and exceptons because the side effect notion wasn’t a use case in my project.

fun callFunction(function: (Int) -> Unit): Unit = 
    (1..2).forEach(function)
fun reciprocal(){}
private val printNumber = { x: Number -> println("Number: $x") }
@Test
fun testCallFunction(){
    assertEquals(UnitStyle.callFunction(printNumber), Unit)
}
@Test
fun testReciprocal(){
    assertEquals(UnitStyle.reciprocal(), Unit)
}

gist: unit

Any

Any is the root of the Kotlin class hierarchy. Any can be used as return type. Declaring Any as return type basically means: any Kotlin type can be returned. Null is not a type – so it can not be returned for a defined return type Any.
Any is an option to wrap exceptions into a Kotlin type. However, dealing with the exceptional case isn’t improved compared to the exception handling approach.

fun parse(s: String): Any =
  when(s.matches(Regex("-?[0-9]+"))){
    true -> s.toInt()
    false -> NumberFormatException(
      "$s is not a valid integer."
    )
  }
fun testParse(){
   assertEquals(AnyStyle.parse("42"), 42, "42 expected")
   assertEquals(
       AnyStyle.parse("some string").toString(), 
       NumberFormatException(
            "some string is not a valid integer.")
                .toString(), 
            "some string can not be parsed"
        )
    }

gist: any

Nullables

A union type of a specific Kotlin type like String and null is a Nullable: String?
A string or null will be returned if a function return type is defined as String?

The Elvis operator ?: helps you deal with Nullable return values. The !! operator is an option once you are sure null isn’t the actual returned value. However in case the value is null the !! operator throws an exception.

I applied this approach in tests of my code because the code lines are short. However for dealing with business logic I continued my research for another approach.

fun parse(s: String): Int? =
    when(s.matches(Regex("-?[0-9]+"))){
        true -> s.toInt()
        false -> null
    }
//kotlinlang.org/docs/reference/null-safety.html#elvis-operator
@Test
fun testParseElvisOperator(){
  val parseResult = NullableStyle.parse("some string") ?: -1
  assertEquals(
    parseResult, 
    -1, 
    "Elvis operator turns it into -1"
  )
}

//kotlinlang.org/docs/reference/null-safety.html#the--operator !! - operator
@Test
fun testParseDoubleExclamationOperator(){
  val parseResultForSureNotNull = 
    NullableStyle.parse("42")!!.div(42)
  assertEquals(
    parseResultForSureNotNull, 
    1, 
    "42/42 must be one"
  )
}

gist: nullable

LateInit

Non-null type initialization is not always possible within a constructor.
Lateinit allows to later initialize such a property. Such a property is null until initialized. You should use .isInitialized to check whether such a variable is initialized. This function is key to avoid throwing an exception.

Although the exception handling is slightly improved with .isInitialized there were a lot of .isInitialized calls in my code that were neither null nor exception specific for the domain. Note: LateInit can not be used for primitive types.

data class MyInt(val value: Int)

private lateinit var toBeDefinedInt : MyInt
fun parse(s: String): Int =
    when (s.matches(Regex("-?[0-9]+"))) {
        true -> s.toInt()
        false -> when (this::toBeDefinedInt.isInitialized) {
            true -> toBeDefinedInt.value
            false -> 79
        }
    }
@Test
fun testParse() {
    assertEquals(LateInitStyle.parse("42"), 42, "42 expected")
    assertEquals(
        LateInitStyle.parse("some string"), 
        79, 
        "some string shall be parsed to 79."
    )
}

gist: lateinit

NotNull Delegate

Another option to initialize a property not during object construction time but at a later time are notnull delegates. Trying to read such a property before the initial value has been assigned results in an exception.

Similar to lateInit you risk an exception if you did not initialize before first read.

var toBeDefinedInt : Int by Delegates.notNull()
fun parse(s: String): Int {
    toBeDefinedInt = 79
    return when(s.matches(Regex("-?[0-9]+"))){
        true -> s.toInt()
        false -> toBeDefinedInt
    }
}
@Test
fun testParse() {
    assertEquals(
        NotNullDelegateStyle.parse("42"), 
            42, 
            "42 expected"
    )
    assertEquals(
        NotNullDelegateStyle.parse("some string"), 
            79, 
            "some string shall be parsed to 79."
    )
}

gist: notnull-delegate

Sealed Class

Sealed classes are “enum classes 2.0”. Sealed class can have subclasses:

  • data classes
  • objects

Those subclasses can be used to wrap values, null and exception into Kotlin types. With that you can push basically everything in Kotlin to the type system. When – as expression or statement – makes it easy to react on all subclasses/types.

The Kotlin compiler will complain if not every possible argument value in a when block is covered. I avoid using else in when blocks to make sure all subclasses/types are covered explicitly.

sealed class ParseResult {
    data class IntResult(val value: Int): ParseResult()
    data class Exception(val error: String): ParseResult()
}

fun parse(s: String): ParseResult =
  when(s.matches(Regex("-?[0-9]+"))){
    true -> ParseResult.IntResult(s.toInt())
    false -> ParseResult.Exception(
      NumberFormatException(
        "$s is not a valid integer."
      ).message.toString()
    )
  }
@Test
fun testParse(){
  assertEquals(SealedStyle.parse("42"), 
    ParseResult.IntResult(42), "42 expected")
  assertEquals(SealedStyle.parse("some string"), 
    ParseResult.Exception(
      "some string is not a valid integer."), 
      "some string can not be parsed"
    )

  when(val r = SealedStyle.parse("42")) {
    is ParseResult.IntResult -> assertEquals(r.value, 42)
    is ParseResult.Exception -> fail(r.error)
  }
}

gist: sealed-classes


There are also libraries that help dealing with nulls and exceptions:

Annotations – Nullable

An element annotated with @Nullable (org.jetbrains.annotations.Nullable) claims null value is perfectly valid to return for methods, pass to parameters and hold local variables and fields. You may use this in combination with Nullables. As a developer you still need to deal with null. This wasn’t convincing for me in the project that triggered this research.

@Nullable
fun parse(s: String): Int? =
    when(s.matches(Regex("-?[0-9]+"))){
        true -> s.toInt()
        false -> null
   }
@Test
fun testParse() {
   assertEquals(NullableAnnotationStyle.parse("42"), 
     42, "42 expected"
   )
   assertEquals(NullableAnnotationStyle.parse("some string"), 
     null, "some string can not be parsed"
   )
}

gist: annotations—nullable

Λrrow

Λrrow is a library for typed functional programming (FP) in Kotlin to empower users to write pure FP apps and libraries built atop higher order abstractions.

Λrrow Option

Option can contain some or none. In case a value is provided, you can expect some. In case null or an exception would be thrown, you can expect none.

Deprecated: Option will be deleted soon as it promotes the wrong message of using a slower and memory unfriendly abstraction when the lang provides a better one.

https://arrow-kt.io/docs/apidocs/arrow-core-data/arrow.core/-option/

On compile (kotlin 1.4.0 & arrow 0.11) time you’d experience a warning similar to:

'Option<out A>' is deprecated. Option will be deleted 
soon as it promotes the wrong message of using a slower 
and memory unfriendly abstraction when the lang provides 
a better one. Alternatively, if you can't support nulls, 
consider aliasing Either<Unit, A> as described here 
https://github.com/arrow-kt/arrow-core/issues/114#issuecomment-641211639

Similar to sealed classes Option nicely wraps success cases and error cases in a type. This comes with a price:

  • an additional library is part of the code
  • the deprecation of the option type
fun parse(s: String): Option<Int> =
    when(s.matches(Regex("-?[0-9]+"))){
        true -> Some(s.toInt())
        false -> None
    }
fun testParseWithOption() {
  assertTrue(OptionStyle.parse("42").isDefined(), "some expceted")
  assertEquals(OptionStyle.parse("42"), Some(42), "some containing 42 expceted")
  assertTrue(OptionStyle.parse("some string").isEmpty(), "none expected")
  assertEquals(OptionStyle.parse("some string"), None, "none expected")
}

gist: option

Λrrow Either

Either can contain right or left values. By convention, the right side of an Either is used to hold successful values. The left side can be used for not successful values.

Similar to Option and sealed classes Either wraps success cases and error cases very well into a type. Unlike Option it is not deprecated.

fun parse(s: String): Either<NumberFormatException, Int> =
  when(s.matches(Regex("-?[0-9]+"))){
    true -> Either.Right(s.toInt())
    false -> Either.Left(
      NumberFormatException("$s is not a valid integer.")
    )
  }
@Test
fun testParse() {
  assertTrue(EitherStyle.parse("42").isRight(), 
    "42 is expected"
  )
  EitherStyle.parse("42")
    .right()
    .shouldBeRight(Right(42))
  assertTrue(EitherStyle.parse("some string").isLeft(),
    "some string can not be parsed"
  )
  EitherStyle.parse("some string")
    .left()
    .shouldBeLeft()

gist: either

Code

Find the sample code here:

Conclusion

Sealed classes is the best option in my project to deal with exceptions and null. There was no need for me to add a library. However, pure functional programming in Kotlin with Λrrow is a great option. I will take this into account for a future project..

Links

Be the first to comment

Leave a Reply

Your email address will not be published.


*


This site uses Akismet to reduce spam. Learn how your comment data is processed.