Handle process statuses with Kotlin Sealed Classes

syIsTyping
don’t code me on that
4 min readFeb 14, 2020

Do you have scenarios where different handling needs to be done according to the status of a process? Something like:

data class Result(
val isSuccess: Boolean,
val value: String?, // non-null when isSuccess is true
val errorMessage: String? // non-null when isSuccess is false
)

private fun process(): Result {
return ... // do something and return a Result
}

fun main() {
val result = process()
val text: String = if (result.isSuccess) {
println(result.value!!)
} else {
println(result.errorMessage!!)
}
println(text)
}

This approach has several shortfalls:

  • The consumer of Result always has to remember to check the isSuccess attribute before handling it, otherwise it may access the wrong attribute.
  • The Result class contains the union of all the attributes of all the possible statuses (in this case, success or failure). It’s not possible to know which attributes belong to which status, and which attributes are present. We’ll have to use comments.
  • The non-status attributes have to be nullable, or have some default/dummy value.
  • Since the attributes are nullable, some sort of null handling needs to be done (ie, we still need to handling nulls even though we may be sure that when we get a success result, the value will never be null)

If we extend a bit further, let’s say that Result can have 3 status types:

enum class Status {
SUCCESS,
FAILURE,
PENDING
}

data class Result(
val status: Status,
val value: String?, // non-null when SUCCESS
val errorMessage: String? // non-null when FAILURE
)

private fun process(): Result {
return ... // do something and return a Result
}

fun main() {
val result = process()
val text: String = when(result.status) {
Status.SUCCESS -> result.value!!
Status.FAILURE -> result.errorMessage!!
Status.PENDING -> "no attributes are available"
else -> "undefined status"
}
println(text)
}

This has even more shortfalls:

  • We now need to define an enum Status to maintain the possible statuses
  • We need an else clause in the when conditional, even though we know that we have handled all the statuses and the else clause will never be reached.

There’s a simpler, more elegant approach using Sealed Classes. The description in the documentation is quite confusing, so hopefully this is clearer: Sealed Classes are a set of parent/children classes that must be defined on the same file, the usual superclass/subclass relationship and restrictions apply. The parent class is defined with a sealed modifier. Due to this, the complier “knows” the exhaustive list of children classes and can perform some magic as we will see. As a result, the children classes share similar traits as enums.

With Sealed Classes, we can make the following changes to the code above.

  • 1) Since they act like enums, we don’t need another enum class. We can define Result as a sealed class and then represent the status as its children.
  • 2) As each status is a subclass, it can define attributes specific to itself, including defining the nullability.
sealed class Result
data class Success(val value: String) : Result()
data class Failure(val errorMessage: String) : Result()
object Pending : Result()
  • 3) The consumer of Result is now mandated by the compiler to check for statuses, otherwise it only has a Result class with no attributes
  • 4) The actual check is simplified as the compiler is smarter now — it knows the full list of possible statuses.
  • 5) Kotlin has smart-casts, and paired with the when clause, we can access the attributes directly, and without unnecessary null checks. More importantly, there is no way to access the wrong attribute accidentally.
val text: String = when(result) {
is Success -> result.value
is Failure -> result.errorMessage
is Pending -> "no attributes are available"
}

Pros

  • We got rid of the redundant Status class. The status metadata is now “encoded” into the type itself.
  • We got rid of redundant null types! It is also very clear now which status owns which attributes, and it is immediately apparent to the consumer which attributes are available.
  • The consumer is forced by compiler to check the status, it doesn’t have the chance to forget and misuse attributes.
  • If we add a new status, the compiler will force us to add it to the when clause. Previously when using enums, we may forget to check the new status because the compiler doesn’t care!
  • The code is slightly less verbose and slightly more fluent. Compare reading when result.status == Status.SUCCESS vs when result is Success.
  • Since each status is a class by itself, we can define different functions on it, or implement different behaviour without further conditional checks for status. (Eg, not saying it’s a good idea, but we could implement different toString() for each status)

Full code with sealed classes

sealed class Result
data class Success(val value: String) : Result()
data class Failure(val errorMessage: String) : Result()
object Pending : Result()

private fun process(): Result {
return ... // do something and return a Result
}

fun main() {
val result = process()
val text: String = when(result) {
is Success -> result.value
is Failure -> result.errorMessage
is Pending -> "no attributes are available"
}
println(text)
}

--

--

syIsTyping
don’t code me on that

Security engineer and new dad in Japan. I've learnt a lot from the community, so I hope to contribute back. I write technical articles and how-to guides.