Kotlin's Billion-Dollar Fix: Null Lives in the Type System


Tony Hoare called the null reference his “billion-dollar mistake” — a single design choice that produced decades of crashes. Java inherited it whole: any reference can be null, and the compiler never makes you check. Kotlin’s fix isn’t a runtime guard or a linter; it moves nullability into the type system, so the question “could this be null?” has an answer the compiler enforces.

We met this idea briefly in the types post and again through smart casts. This post is the full toolkit.

Two types, not one

String and String? are different types. The first can never hold null; the second might. That single distinction is the whole foundation:

val name: String = "Ada"      // never null
val maybe: String? = null     // the ? opts into null

Once a value’s type ends in ?, the compiler refuses to let you use it as if it were always present. You have to handle the null case — and the rest of this post is the vocabulary for doing that without ceremony.

The safe call: ?.

?. calls a member only if the receiver is non-null; otherwise the whole expression is null:

val length: Int? = maybe?.length     // null if maybe is null

Safe calls chain, and the chain short-circuits the moment anything in it is null — no nested null checks:

val city: String? = user?.address?.city

The Elvis operator: ?:

A safe call leaves you with a nullable result. ?: supplies a fallback for when the left side is null (the name is a joke about Elvis Presley’s hair, if you tilt your head):

val length: Int = maybe?.length ?: 0
val city: String = user?.address?.city ?: "unknown"

It pairs naturally with early exits, because the right-hand side can be a return or throw:

val token = request.token ?: return null
// past this line, token is a non-null String

The escape hatch: !!

!! asserts “I know this isn’t null” and throws a NullPointerException if you’re wrong. It converts T? to T by force:

val length: Int = maybe!!.length     // throws if maybe is null

Treat every !! as a small confession that you know something the compiler doesn’t. Occasionally that’s true. More often it’s a safe call or an Elvis waiting to be written instead.

Safe casts: as?

A plain cast (as) throws if the type is wrong. The safe cast as? returns null instead, which composes beautifully with Elvis:

val count = value as? Int ?: 0       // 0 if value isn't an Int

Doing something only when non-null: ?.let

When you want to run a block only if a value is present, combine the safe call with let. Inside the block, the value is non-null and named it:

user.email?.let { address ->
    sendWelcome(address)             // runs only if email != null
}

This is the idiomatic replacement for if (x != null) { ... } when x is a property rather than a local variable. (Smart casts handle the local-variable case for you; ?.let covers the cases they can’t.)

lateinit: non-null, but not yet

Sometimes a property genuinely can’t be set in the constructor — think dependency injection or a test’s @BeforeEach. You don’t want to make it nullable just to satisfy initialization. lateinit var promises the compiler you’ll assign it before first use:

lateinit var repository: UserRepository

fun setUp() {
    repository = UserRepository()
}

Read it before it’s assigned and you get a clear UninitializedPropertyAccessException, not a vague NPE. Use it only when you control the lifecycle; for everything else, a nullable type is honest.

Nulls in collections

A List<String?> can contain nulls; a List<String>? is a whole list that might be null. The distinction matters, and the standard library has clean tools for the first case:

val names: List<String?> = listOf("Ada", null, "Linus")
val clean: List<String> = names.filterNotNull()   // ["Ada", "Linus"]

The leak from Java: platform types

Here’s the one place the guarantees soften. When Kotlin calls Java code, it can’t know whether a returned String is nullable — Java doesn’t say. Kotlin calls this a platform type, written String!, and it trusts you: no forced null check, but a null still crashes at the point of use.

The defense is to pin the type down the moment a value crosses the boundary:

val name: String = javaApi.getName()   // assert non-null here, fail fast
val name: String? = javaApi.getName()  // or treat it as nullable

If the Java library is annotated with @Nullable/@NonNull, Kotlin honors those and the platform type disappears. We’ll return to this when we cover Java interop directly.

Final thoughts

The win isn’t any single operator — it’s that “can this be null?” stops being a question you answer by reading documentation or crashing in production. The type says it, and the compiler holds you to it. The operators here (?., ?:, as?, ?.let) are just the small, ergonomic moves for the rare cases where the answer is “yes, and I need to handle it.”

Next, we put these types to work in bulk: collections, where Kotlin makes a distinction Java never did — read-only versus mutable.

Practice: reinforce this with the companion workbook — short, click-to-reveal problems.