article banner (priority)

Effective Kotlin Item 7: Prefer a nullable or Result result type when the lack of a result is possible

This is a chapter from the book Effective Kotlin. You can find it on LeanPub or Amazon.

Sometimes a function cannot produce its desired result. A few common examples are:

  • We try to get data from a server, but there is a problem with the internet connection.
  • We try to get the first element that matches some criteria, but there is no such element in our list.
  • We try to parse an object from text, but this text is malformatted.

There are two main mechanisms to handle such situations:

  • Return a null or Result.failure, thus indicating failure.
  • Throw an exception.

There is an important difference between these two. Exceptions should not be used as a standard way to pass information. All exceptions indicate incorrect, special situations and should be treated this way. We should use exceptions only for exceptional conditions (Effective Java by Joshua Bloch). The main reasons for this are:

  • The way exceptions propagate is not very readable for most programmers and might easily be missed in code.
  • In Kotlin, all exceptions are unchecked. Users are not forced or even encouraged to handle them. They are often not well documented, and they are not very visible when we use an API.
  • Because exceptions are designed for exceptional circumstances, there is little incentive for JVM implementers to make them as fast as explicit tests.
  • Placing code inside a try-catch block inhibits certain optimizations that the compiler might otherwise perform.

It is worth mentioning that exceptions are used by some popular patterns, like on backend, exceptions are used to end request processing and respond to requester with a specific response code and message. Similarly on Android, exceptions are sometimes used to end a process and display a concrete dialog or toast to the user. In such cases, many of my arguments against exceptions do not apply, and using exceptions could be reasonable.

On the other hand, null or Result.failure are both perfect for indicating an expected error. They are explicit, efficient, and can be handled in idiomatic ways. This is why the rule is that we should prefer to return null or Result.failure when an error is expected, and we should throw an exception when an error is not expected. Here are some examples:

inline fun <reified T> String.readObjectOrNull(): T? { //... if (incorrectSign) { return null } //... return result } inline fun <reified T> String.readObject(): Result<T> { //... if (incorrectSign) { return Result.failure(JsonParsingException()) } //... return Result.success(result) } class JsonParsingException : Exception()

Using Result result type

We use the Result class from the Kotlin stdlib to return a result that can be either a success or a failure. Failure includes an exception, that keeps the information about the error. We use Result instead of nullable type in functions that need to pass additional information in the case of failure. For example, when we implement a function that is fetching information from the internet, Result should be preferred over null, because we can pass the information about the error, like the error code or the error message.

When we choose to return Result, the user of this function will be able to handle it using methods from the Result class:

userText.readObject<Person>() .onSuccess { showPersonAge(it) } .onFailure { showError(it) }

Using such error handling is simpler than using a try-catch block. It is also safer, because an exception can be missed and can stop our whole application; in contrast, a null value or a Result object needs to be explicitly handled, and it won’t interrupt the flow of the application.

The difference between a nullable result and the Result object is that we should prefer the latter when we need to pass additional information in the case of failure; otherwise, we should prefer null.

The Result class has a rich API of methods you can use to handle your result, including:

  • isSuccess and isFailure properties, which we use to check if the result represents a success or a failure (isSuccess == !isFailure is always true).
  • onSuccess and onFailure methods, which call their lambda expressions when the result is, respectively, a success or a failure.
  • getOrNull method, which returns the value if the result is a success, or null otherwise.
  • getOrThrow method, which returns the value if the result is a success, or throws the exception from the failure otherwise.
  • getOrDefault method, which returns the value if the result is a success, or the default value provided as an argument if the result is a failure.
  • getOrElse method, which returns the value if the result is a success, or calls its functional argument and returns its result.
  • exceptionOrNull method, which returns the exception if the result is a failure, or null otherwise.
  • map method for transforming the success value.
  • recover method for transforming a throwable value into a success value.
  • fold method for handling both success and failure in a single method.

To transform a function that throws exceptions into one that returns Result, use runCatching.

fun getA(): Result<T> = runCatching { getAThrowing() }

Using null result type

In Kotlin, null is a marker of a lack of value. When a function returns null, it means that it cannot return a value. For example:

  • List<T>.getOrNull(Int) returns null when there is no value at the given index.
  • String.toIntOrNull() returns null when String cannot be correctly parsed to Int.
  • Iterable<T>.firstOrNull(() -> Boolean) returns null when there are no elements matching the predicate from the argument.

As you can see, null is used to indicate that a function cannot return the expected value. We use nullable type instead of Result in functions that do not need to pass additional information in the case of failure, where the meaning of null is clear. In the function String.toIntOrNull(), it is clear that null means that the string cannot be parsed to Int. In the function Iterable<T>.firstOrNull(() -> Boolean), it is clear that null means that there are no elements matching the predicate. For all functions that return null, the meaning of null should be clear.

Nullable value needs to be unwrapped before it can be used. For dealing with them, Kotlin offers us many useful features, like the safe call operator ?., the Elvis operator ?:, and smart casting.

val age = userText.readObjectOrNull<Person>()?.age ?: -1 val printer: Printer? = getFirstAvailablePrinter() printer?.print() // Safe call if (printer != null) printer.print() // Smart casting

Null is our friend, not an enemy

Many Kotlin developers are ex-Java developers, who are thought to treat null like an enemy. For example, in Effective Java (2nd edition), Joshua Bloch presents Item 43: Return empty arrays or collections, not nulls. Such a suggestion would be absurd in Kotlin. An empty collection has a completely different meaning than null. Imagine we called the function getUsers: if it returned null, this would mean it couldn’t produce a value, so we still don’t know what the answer is; in contrast, if it returned an empty collection, this would mean that there are no users. These are different results, and they should not be confused. Kotlin's type system lets us express what is nullable and what is not, and it forces us to handle nulls consciously. We should not be afraid of nulls: we should embrace them and use them to express our intentions. Forget about all the suggestions to avoid nulls because they are not applicable in Kotlin. In Kotlin, null is our friend, not an enemynull-friend.

Defensive and offensive programming

In Item 5: Specify your expectations for arguments and state, I explained that we should throw exceptions to signal incorrect arguments or states, and in this item, I explained that we should in general avoid throwing exceptions and prefer returning Result or nullable types instead. These two statements seem to be in conflict, but they are not, because they refer to different kinds of situations.

Exceptions should not be part of our regular program execution flow; so, when you perform an operation that might either succeed or fail, like fetching data from a database or network, you should use Result or nullable types. This forces the developer to handle the failure case explicitly. Since it is part of the regular program execution flow, it is best to handle such a situation safely so all possible situations are handled correctly. This is an implementation of the defensive programming idea.

On the other hand, when a developer makes a mistake, like calling a method with incorrect arguments or calling a method on an object in an incorrect state, silencing such a situation would be dangerous because our program has encountered a situation that is clearly unexpected. We should loudly signal this situation so our program can be corrected. This is an implementation of the offensive programming idea.

Defensive and offensive programming do not contradict each other; they are more like yin and yang: different techniques that are both needed in our programs for the sake of safety, so we need to understand and use both of them appropriately.

Summary

  • When a function can fail, we should return Result or a nullable type instead of throwing an exception.
  • We should use Result when we need to pass additional information in the case of failure.
  • We should use a nullable type when the meaning of null is clear.
  • We should not be afraid of nulls: we should embrace them and use them to express our intentions.
  • We should use defensive programming to handle regular program execution flow, while offensive programming should be used to handle unexpected situations.
nothing:

This is possible because both return and throw declare Nothing as a return type that is a subtype of every type. More about this here: kt.academy/article/kfde-type_system

null-friend:

See Null is your friend, not a mistake by Roman Elizarov. Link: https://kt.academy/l/re-null