10 December 2017
Before Kotlin, I spent a non-trivial chunk of my debugging time chasing NullPointerException crashes. Not because I was careless — Java just didn’t give you the tools to express “this value might be absent” at the type level. You’d sprinkle @Nullable annotations, add defensive if (x != null) checks everywhere, and pray that nobody forgot one. Tony Hoare, who invented null references, famously called them his “billion-dollar mistake.” Having lived through a few years of Java Android development, I believe the estimate is low.
Kotlin’s null safety system doesn’t just reduce NPEs. It fundamentally changes how you model data. When nullability is part of the type system, you’re forced to think about absence at the point of declaration, not at the point of use. That single shift — from “check for null where you use it” to “declare whether null is possible where you define it” — eliminates entire categories of bugs.
In Kotlin, every type has two variants. String cannot be null — the compiler enforces this at compile time. String? can be null. This isn’t an annotation or a hint — it’s a type-level distinction that the compiler tracks through your entire program.
class UserProfileViewModel(
private val userRepository: UserRepository
) : ViewModel() {
// Non-null: guaranteed to always have a value
val screenTitle: String = "Profile"
// Nullable: might be absent
var userName: String? = null
fun loadUser(userId: String) {
viewModelScope.launch {
val user = userRepository.getUser(userId)
userName = user?.displayName
}
}
}
The compiler won’t let you call methods on a nullable type without handling the null case first. This sounds restrictive, but it’s exactly the constraint that prevents NPEs. Instead of finding out about null at runtime — usually in production, usually on a Friday — you handle it at compile time.
The ?. operator is the workhorse of Kotlin null safety. It short-circuits to null if the receiver is null, otherwise calls the method. Where this gets powerful is chaining — you can build long safe call chains that gracefully propagate null through multiple levels.
class NotificationService(
private val userRepository: UserRepository,
private val pushClient: PushClient
) {
suspend fun sendWelcomeNotification(userId: String) {
val user = userRepository.findUser(userId)
// Safe call chain — any null in the chain short-circuits the whole expression
val cityName = user?.address?.city?.name
val token = user?.deviceToken
token?.let { validToken ->
pushClient.send(
token = validToken,
title = "Welcome",
body = "Hello, ${user?.displayName ?: "there"}!"
)
}
}
}
Safe call chains are especially common when dealing with deeply nested data from APIs. A response might have response?.data?.user?.profile?.avatarUrl — instead of writing four nested if checks, you get one expression that’s null if any step fails. In a real Android app, you’ll use this pattern constantly with Intent extras, Bundle arguments, and cursor data where nested nullability is the norm.
let combined with ?. gives you a scoped block that only executes when the value is non-null. But let isn’t the only option — run, also, and apply all have their place depending on what you need.
class OrderProcessor(
private val orderDao: OrderDao,
private val analytics: AnalyticsTracker
) {
suspend fun processOrder(orderId: String?) {
// let — when you need to transform the non-null value
val order = orderId?.let { id ->
orderDao.getOrderById(id)
}
// run — when you need to execute a block on the non-null value
// and return a result (like let but with 'this' instead of 'it')
order?.run {
analytics.trackOrderProcessed(orderId, totalAmount)
}
// also — when you want to do a side effect but keep the original value
order?.also { validOrder ->
analytics.trackEvent("order_viewed", mapOf("id" to validOrder.orderId))
}
}
}
I see a lot of developers overuse let for simple null checks. If all you’re doing is calling one method, ?. alone is usually cleaner. let shines when you need to do multiple things with the non-null value or when you want to avoid repeating the safe call chain. But nesting three levels of ?.let { } blocks is a code smell — at that point, you probably need an early return or a different data structure.
One of Kotlin’s best features is that the compiler tracks null checks across control flow. After you check if (x != null), the compiler knows x is non-null inside that block and automatically casts it. No explicit cast needed.
class PaymentProcessor(
private val paymentGateway: PaymentGateway,
private val analyticsTracker: AnalyticsTracker
) {
fun processPayment(order: Order) {
val discountCode = order.discountCode // String? type
// After this check, discountCode is smart-cast to String (non-null)
if (discountCode != null) {
val discount = calculateDiscount(discountCode)
analyticsTracker.trackDiscountUsed(discountCode, discount)
}
// Early return pattern — also enables smart cast
val shippingAddress = order.shippingAddress ?: run {
analyticsTracker.trackError("No shipping address")
return
}
// shippingAddress is now smart-cast to non-null
paymentGateway.charge(order.total, shippingAddress)
}
}
Smart casts work with if, when, and early returns. They also work with is type checks. The limitation is that smart casts only work on val properties and local variables — not var properties, because the compiler can’t guarantee a var wasn’t changed between the check and the use by another thread. If you find yourself needing to smart-cast a var, assign it to a local val first.
The ?: operator (named for Elvis Presley’s hairstyle, apparently) provides a default value when the left side is null. It’s concise and avoids the if (x != null) x else default boilerplate that Java developers write constantly.
class UserSettingsRepository(
private val sharedPrefs: SharedPreferences
) {
fun getTheme(): String {
return sharedPrefs.getString("theme", null) ?: "system_default"
}
fun getMaxCacheSize(): Long {
// Elvis with throw — fail fast when null is unexpected
val configValue = remoteConfig.getLong("max_cache_mb")
?: throw IllegalStateException("max_cache_mb config missing")
return configValue * 1024 * 1024
}
fun getUserDisplayName(user: User?): String {
// Chain of fallbacks
return user?.displayName
?: user?.email?.substringBefore("@")
?: "Anonymous"
}
}
The Elvis operator combined with throw or return is particularly powerful. val id = args?.getString("id") ?: return is an early-exit pattern that keeps the rest of your function free from null handling. I use this constantly in Fragment onViewCreated when pulling arguments from the bundle.
The !! (non-null assertion) operator is Kotlin’s escape hatch. It tells the compiler “I know this isn’t null, trust me” and throws a KotlinNullPointerException if it is. IMO, using !! in production code is almost always a mistake. It’s reintroducing the exact problem Kotlin’s type system was designed to solve.
There are exactly two places where I think !! is acceptable. First, in test code where a null value means the test should fail anyway. Second, immediately after a framework call where you know the result is non-null but the return type is nullable due to Java interop. And even in the second case, requireNotNull() with a descriptive message is better.
Both functions throw an exception when the value is null, but they communicate different intent. requireNotNull throws IllegalArgumentException — it means “this argument must not be null, the caller made a mistake.” checkNotNull throws IllegalStateException — it means “this value should not be null given the current state, something is wrong internally.”
class UserProfileFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// requireNotNull — this is a caller/navigation issue
val userId = requireNotNull(arguments?.getString("user_id")) {
"UserProfileFragment requires a user_id argument"
}
// checkNotNull — this is an internal state issue
val binding = checkNotNull(_binding) {
"Binding accessed after onDestroyView"
}
}
}
The distinction matters for debugging. When you see IllegalArgumentException in a crash log, you know to look at the calling code. When you see IllegalStateException, you know to look at the internal state management. This is a small thing, but in a production app with thousands of crashes a week, every bit of signal in the stack trace helps.
This is the area that catches the most people off guard. When you call Java code from Kotlin, the return types aren’t String or String? — they’re String!, which Kotlin calls a “platform type.” The compiler doesn’t know if the value can be null because Java doesn’t have that information in its type system.
Platform types are dangerous because the compiler won’t force you to handle null. If you assign a platform type to a String variable and it’s actually null, you get an NPE at the assignment — not at the point of use. The safest approach is to always treat Java return values as nullable until you’ve verified otherwise. When wrapping a Java API, declare your Kotlin return type explicitly as nullable and handle it properly.
// Java class — no nullability annotations
// public class LegacyUserManager {
// public String getCurrentUserId() { ... }
// }
class AuthRepository(
private val legacyManager: LegacyUserManager
) {
fun getCurrentUserId(): String? {
// Treat Java return as nullable explicitly
// Even if it's "never null," defensive coding is cheap
return legacyManager.currentUserId
}
fun requireCurrentUser(): String {
return legacyManager.currentUserId
?: throw IllegalStateException(
"Expected authenticated user but got null from LegacyUserManager"
)
}
}
If you maintain Java libraries that Kotlin code consumes, add @Nullable and @NonNull annotations to your public API. Kotlin reads these annotations and converts platform types into proper nullable/non-nullable types. It’s a small effort that prevents a lot of bugs downstream.
There’s a difference between a nullable collection and a collection of nullable elements, and mixing them up causes subtle bugs. List<String>? is a list that might not exist. List<String?> is a list that always exists but might contain null elements. List<String?>? is both.
class SearchRepository(
private val searchApi: SearchApi
) {
// API might return null for the entire list (no results found)
suspend fun search(query: String): List<SearchResult>? {
return searchApi.search(query)
}
// Working with nullable collections safely
fun processResults(results: List<SearchResult>?) {
// orEmpty() converts null to empty list — very useful
val safeResults = results.orEmpty()
// filterNotNull() removes null elements from List<T?>
val validResults = results?.filterNotNull().orEmpty()
// isNullOrEmpty() checks both conditions at once
if (results.isNullOrEmpty()) {
showEmptyState()
}
}
}
In real Android apps, nullable collections show up constantly. Cursor queries might return null, API responses might omit arrays entirely, and Bundle.getStringArrayList() returns ArrayList<String>?. The orEmpty() extension is one of the most useful Kotlin stdlib functions — it converts a nullable collection to an empty non-null one, letting you use forEach, map, filter without null checks.
Android’s APIs are full of nullability, and knowing the common patterns saves a lot of debugging time.
Intent extras — Almost everything from an Intent is nullable. intent.getStringExtra("key") returns String?. Always handle this, even if “the other Activity definitely sends it.” After process death, the Intent survives but your assumptions about who launched it might not hold.
Bundle arguments — Same story. arguments?.getString("user_id") is the safe pattern. Never use arguments!!.getString("user_id")!! — it’ll crash after process death when arguments might be reconstructed differently.
Cursor data — cursor.getString(columnIndex) can return null if the column contains a SQL NULL value. When reading from Room or ContentProvider cursors, always handle null columns even if you “know” they’re NOT NULL in the schema — migrations and data corruption happen.
class IntentParser {
fun parseDeepLink(intent: Intent): DeepLinkData? {
// Every step is nullable — handle it gracefully
val uri = intent.data ?: return null
val path = uri.path ?: return null
val segments = uri.pathSegments
if (segments.isNullOrEmpty()) return null
return when (segments.firstOrNull()) {
"order" -> {
val orderId = segments.getOrNull(1) ?: return null
DeepLinkData.Order(orderId)
}
"profile" -> {
val userId = uri.getQueryParameter("id") ?: return null
DeepLinkData.Profile(userId)
}
else -> null
}
}
}
Here’s the insight that took me a while to arrive at: Kotlin’s null safety isn’t really about preventing NullPointerException. It’s about forcing you to model your data correctly. When you declare a property as String?, you’re saying “absence is a valid state for this value.” When you declare it as String, you’re saying “this value is always present.”
That distinction pushes you to think about your data model earlier and more carefully. Should this user always have an email, or is it optional? Can a payment exist without a shipping address, or is that invalid? In Java, these questions were answered implicitly (usually by whoever happened to set the field first). In Kotlin, they’re answered explicitly in the type declaration, and the compiler holds you to your answer.
The result isn’t just fewer crashes. It’s code that communicates intent more clearly, data models that match business rules more accurately, and a codebase where null is a conscious decision rather than an accidental omission.
Thanks for reading!