Skip to content
beginner Phase 1 · Kotlin Foundations

Null Safety

Master nullable types, safe calls, let, Elvis operator, and non-null assertions.

40m
3 problems
Topic Progress 0%

Nullable Types and the Type System

The Billion-Dollar Mistake

Tony Hoare called null references his "billion-dollar mistake." NullPointerExceptions account for roughly 40% of all runtime errors in Java. Kotlin's type system addresses this directly by making nullability explicit at the type level.

Non-Nullable vs Nullable Types

By default, every type in Kotlin is non-nullable. You cannot assign null to it:

val name: String = "Kotlin"
// name = null  // Compile error: Null can not be a value of a non-null type String

To allow null, append ? to the type:

val nullableName: String? = null   // Allowed
val alsoValid: String? = "Kotlin"  // Also allowed

This is a compile-time distinction. The JVM still uses null internally, but Kotlin's compiler prevents null from propagating through non-nullable types. You handle nulls explicitly where they can occur.

Why This Matters

In Java, every reference can be null. This means every method call, every field access, every array element access can throw a NullPointerException. Developers must mentally track which references might be null across entire call chains.

Kotlin forces you to address null at the point of assignment. If a variable is String?, the compiler requires you to handle the null case before using it as a String. This shifts null handling from runtime to compile time.

Null in Collections

Kotlin distinguishes between a collection of nullable elements and a nullable collection:

val nonNullList: List<String> = listOf("a", "b")
// nonNullList cannot contain null elements

val nullableList: List<String?> = listOf("a", null, "b")
// Elements can be null

val nullableListOfStrings: List<String>? = null
// The list itself can be null

Platform Types

When interoperating with Java code, Kotlin cannot determine nullability from Java annotations. It uses platform types (String!, Int!, etc.), which behave like nullable types for safety:

// Java method: public String getName() { ... }
val javaName: String = javaObject.name  // Compiler trusts you
val safeName: String? = javaObject.name // Safer: null-checked

Always treat Java return values as nullable unless you can verify nullability through annotations (@Nullable, @NonNull).

Safe Calls, Let, and the Elvis Operator

The Safe-Call Operator (?.)

The safe-call operator chains property access or method calls that might return null. If any part of the chain is null, the entire expression returns null:

val name: String? = "Kotlin"
println(name?.length)  // 6

val nullName: String? = null
println(nullName?.length)  // null (no exception)

Without the safe-call operator, accessing .length on a nullable String would require an explicit null check.

Safe Calls with let

The let function scopes a non-null value to a block. Combined with ?.let, it executes code only when the value is non-null:

val name: String? = "Kotlin"

name?.let {
    println("Name has ${it.length} characters")
    // 'it' is the non-null String inside this block
}
// If name is null, this block is skipped entirely

This is the idiomatic way to perform null-conditional operations:

fun processUser(user: User?) {
    user?.let {
        println("Processing ${it.name}")
        sendEmail(it.email)
        logAccess(it.id)
    }
    // No else branch needed; null is silently ignored
}

The Elvis Operator (?:)

The Elvis operator provides a default value when the left side is null. It reads as "if not null, use this; otherwise, use that":

val name: String? = null
val displayName = name ?: "Anonymous"
println(displayName)  // Anonymous

val length = name?.length ?: 0
println(length)  // 0

The Elvis operator is essential when you need a concrete value (not nullable) from a nullable source:

fun getConfigValue(key: String): String {
    return cache[key] ?: database[key] ?: defaultConfig[key] ?: ""
}

This chain tries multiple sources and falls back to an empty string.

Non-Null Assertions (!!)

The !! operator converts a nullable type to its non-null counterpart, throwing a NullPointerException if the value is null:

val name: String? = null
println(name!!.length)  // Throws NullPointerException

Use !! only when you are certain the value is non-null and cannot prove it to the compiler. Common scenarios:

  • Asserting invariants after a previous check
  • Quick prototyping (but remove before production)
  • Working with Java APIs that are known non-null but not annotated

Overusing !! defeats the purpose of Kotlin's null safety system.

Safe Casting (as?)

The safe cast operator returns null if the cast fails, instead of throwing a ClassCastException:

val obj: Any = "Hello"
val str: String? = obj as? String   // "Hello"
val num: Int? = obj as? Int          // null

This is safer than as for conditional casting:

when (val result = obj as? String) {
    is String -> println("String of length ${result.length}")
    null -> println("Not a string")
}

Practice Problems

0 / 3 solved
Safe Length Calculator

Write a function safeLength that takes a nullable String? and returns its length, or 0 if null. Use the Elvis operator. Then write a version using let.

Solution
fun safeLength(name: String?): Int {
    return name?.length ?: 0
}

fun safeLengthUsingLet(name: String?): Int {
    return name?.let { it.length } ?: 0
}

fun main() {
    println(safeLength("Kotlin"))  // 6
    println(safeLength(null))       // 0
}
Null-Safe User Profile

Given a nullable User object, print the user's name and email. If the user is null, print 'No user'. If the email is null, print 'No email' instead of the email. Use safe calls and the Elvis operator.

Solution
data class User(val name: String, val email: String?)

fun printProfile(user: User?) {
    val name = user?.name ?: "No user"
    val email = user?.email ?: "No email"
    println("Name: $name, Email: $email")
}

fun main() {
    printProfile(User("Alice", "alice@example.com"))
    // Name: Alice, Email: alice@example.com

    printProfile(User("Bob", null))
    // Name: Bob, Email: No email

    printProfile(null)
    // Name: No user, Email: No email
}
Chained Safe Calls

Given a nested data structure (Company has a CEO who has a name), write a function that safely retrieves the CEO name or returns 'Unknown' if any part of the chain is null.

Solution
data class Person(val name: String)
data class Company(val ceo: Person?)

fun getCeoName(company: Company?): String {
    return company?.ceo?.name ?: "Unknown"
}

fun main() {
    println(getCeoName(Company(Person("Andy"))))  // Andy
    println(getCeoName(Company(null)))              // Unknown
    println(getCeoName(null))                       // Unknown
}

Quiz

1. What does the safe-call operator (?.) do?

Question 1 options

2. What is the result of `val x: String? = null; val y = x ?: "default"`?

Question 2 options

3. What happens when you use the !! operator on a null value?

Question 3 options

4. What does `name?.let { it.length }` return when name is null?

Question 4 options

Flashcards

Question

How do you declare a nullable type in Kotlin?

Answer

Append ? to the type. Example: val name: String? = null. Non-nullable types cannot hold null at compile time.

Question

What is the difference between ?.let and !!?

Answer

?.let executes a block only if the value is non-null, returning null otherwise. !! converts to non-null and throws NullPointerException if null. Prefer ?.let for safe handling.

Question

When should you use the !! operator?

Answer

Only when you are certain the value is non-null and cannot prove it to the compiler. Common in Java interop or asserting invariants. Avoid overusing it as it defeats null safety.

Question

What is a platform type in Kotlin?

Answer

A type (marked as String!, Int!, etc.) that comes from Java code where nullability is unknown. Kotlin treats it as nullable for safety, but the compiler does not enforce null checks on it.

Revision Notes

Key Takeaways

  • 1. Kotlin's type system distinguishes nullable and non-nullable types at compile time.
  • 2. Use ?.let to execute code only when a value is non-null.
  • 3. The Elvis operator (?:) provides defaults for nullable values.
  • 4. Reserve !! for cases where nullability cannot be proven to the compiler.
  • 5. Treat all Java return values as nullable unless verified with annotations.

Interview Tips

  • Explain why Kotlin's null safety is superior to Java's null handling.
  • Be ready to demonstrate safe-call chaining with ?. and ?:.
  • Discuss when !! is acceptable and when it is a code smell.
  • Know how platform types work when calling Java from Kotlin.

Cheat Sheet

Null Safety Cheat Sheet

Nullable Types:

  • String? can hold null; String cannot
  • Compile-time enforcement prevents NPEs

Safe Call (?.):

  • name?.length returns Int? (null if name is null)
  • Chains: user?.address?.city

Elvis (?:):

  • name?.length ?: 0 returns 0 if name is null
  • Provides a default value

Non-Null Assertion (!!):

  • name!!.length throws NPE if name is null
  • Use sparingly; defeats null safety

Safe Let:

  • name?.let { block } executes only if non-null
  • it is the non-null value inside the block

Safe Cast (as?):

  • obj as? String returns null if cast fails