Be Careful of toString() on Nullable Types in Kotlin

Brian Terczynski
3 min readJan 15, 2023

--

Photo by Steve Johnson on Unsplash

Wanted to share something that has bitten me with Kotlin’s toString(): be careful of using it on nullable types!

Let’s imagine we have a function navIdToURL(navId: Int): URL?that takes a “navigation ID” and converts it into a URL. We then need to pass that URL to a navigation function navigateToUrl(urlString: String), but that navigation function needs a string version of it (odd, yes, but let’s say it’s an API we don’t control [also just illustrating a point here]). So we need to convert it to a string. We can do that with the venerable toString() function. And there’s one more catch: the navIdToURL() function can return null if it can’t find a matching URL for the given navigation ID.

So we write our code to get the URL and pass it to navigateToUrl():

navigateToUrl(URLResolver().navIdToUrl(navId).toString())

This works fine ifnavIdToURL() returns a non-null value. But we’re totally ignoring the fact that it can return null.

One of the nice things about Kotlin is that it forces you to deal with nullable types when you try to use them in non-nullable places. navigateToUrl() does not allow nulls. And yet, we’re passing the value returned from navIdToURL , which allows nulls, right into this non-null parameter. The compiler does not complain. The code runs. Why?

It has to do with the toString() function that we’re using. We’re not actually using the toString() member function as overridden by the URL class (not directly, that is). Instead, we’re using the Kotlin extension function kotlin.toString() which allows for a null receiver. According to the documentation:

What’s happening is toString() is intercepting the nullable object and always returning a non-null object. That’s why our code compiles and runs.

But it crashes if navIdToURL returns null because we pass in the malformed URL string “null” to navigateToUrl().

This is something for which to be very careful with the toString() function, because it is very easy to just use that extension function on a nullable object and not realize that you’re not properly handling a null value.

One way to fix this would be to make sure you use the Kotlin safe operator ?. on the very first link in a chain that returns null. That paired with a ?.let statement means we could write something like this:

    URLResolver().navIdToURL(navId)?.toString()?.let {
navigateToUrl(it)
} ?: handleUnrecognizedUrl(navId)

This fixes the issue, but you’d need to remember to do this (I for one have forgotten).

Another thing you could try, to avoid inadvertent use of kotlin.toString() on a nullable type, is to use an import alias for it, e. g.:

import kotlin.toString as toStringOfNullableType

That way, if you try to use toString() on a nullable type in your file, the compiler will use the toString() member function of the object and then complain:

But that technique only works on a per-file basis and is quite frankly a pain.

Probably the best thing would be to have a linter detect these cases for you. For instance, detekt has a rule for just this case.

Really, the point behind all of this is to be careful how you use extension functions on nullable types. You likely will want to handle null values in special ways, but if you apply extension functions to them before handling them then you may get unexpected results that you won’t catch at compile time. The toString() extension function is just one example of this. Pay close attention to these cases and if you can, adopt best practices or lint rules around them. The toString() case has certainly bitten me in the past.

--

--

Brian Terczynski
Brian Terczynski

Written by Brian Terczynski

Documenting my learnings on my journey as a software engineer.

No responses yet