In the previous tutorial, you learned about variables and types. Now let’s learn about null safety — the feature that makes Kotlin truly special.

NullPointerException (NPE) is the most common crash in Java and many other languages. Tony Hoare, who invented null references in 1965, called it his “billion-dollar mistake”. Kotlin solves this problem at compile time.

In this tutorial, you will learn:

  • Why null is dangerous
  • How Kotlin prevents null problems
  • All null safety operators: ?, ?., ?:, !!
  • The let function for null handling
  • Smart casts
  • Safe casting with as?
  • lateinit for late initialization

The Problem with Null

In Java, any reference type can be null:

// Java — this compiles fine
String name = null;
int length = name.length(); // CRASH: NullPointerException at runtime

The program compiles without errors. But it crashes when you run it. This is dangerous because:

  • The compiler does not warn you
  • The crash happens at runtime, possibly in production
  • It is hard to find all places where null could appear

Kotlin’s Solution: Nullable Types

In Kotlin, variables cannot be null by default:

// This does NOT compile
val name: String = null // Error: Null cannot be a value of a non-null type String

If you want a variable to be nullable, you must add ? to the type:

// This compiles — ? means "this can be null"
val name: String? = null
val email: String? = "alex@example.com"

Think of String and String? as two different types:

  • String — always has a value, never null
  • String? — might have a value, might be null

The compiler knows the difference and forces you to handle null.

Safe Call Operator: ?.

The safe call operator ?. calls a method only if the value is not null. If the value is null, it returns null instead of crashing.

val name: String? = "Alex"
val nickname: String? = null

println(name?.length)     // 4
println(nickname?.length) // null (not a crash!)

You can chain safe calls:

println(name?.uppercase())     // ALEX
println(nickname?.uppercase()) // null

Multiple safe calls in a chain:

val city: String? = "Berlin"
println(city?.first()?.uppercase()) // B

If any value in the chain is null, the entire expression returns null.

Elvis Operator: ?:

The Elvis operator ?: provides a default value when the expression is null. The name comes from Elvis Presley — ?: looks like his hairstyle if you turn it sideways.

val name: String? = null

// If name is null, use "Unknown"
val displayName = name ?: "Unknown"
println(displayName) // Unknown

When the value is not null, the Elvis operator returns the original value:

val email: String? = "sam@example.com"
val displayEmail = email ?: "no email"
println(displayEmail) // sam@example.com

Combine safe call with Elvis for a common pattern:

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

Elvis with Return

You can use Elvis to exit a function early:

fun processInput(input: String?) {
    val text = input ?: return // Exit if input is null
    println("Processing: $text")
}

Elvis with Throw

You can use Elvis to throw an exception:

fun getRequiredName(name: String?): String {
    return name ?: throw IllegalArgumentException("Name is required")
}

Not-Null Assertion: !!

The !! operator tells the compiler: “I am sure this is not null.” It converts a nullable type to a non-null type.

val name: String? = "Jordan"
val length = name!!.length // 6

Warning: If the value IS null, !! throws NullPointerException:

val name: String? = null
val length = name!!.length // CRASH: NullPointerException

Avoid !! in production code. It defeats the purpose of null safety. Use ?. or ?: instead. The only time !! is acceptable is when you are absolutely certain the value cannot be null and the compiler cannot figure it out.

The let Function

let is a function that runs a block of code on a value. When combined with ?., it runs the block only if the value is not null.

val email: String? = "alex@example.com"
val phone: String? = null

// Runs the block because email is not null
email?.let {
    println("Sending email to: $it")
    println("Email length: ${it.length}")
}

// Does NOT run the block because phone is null
phone?.let {
    println("This will not print")
}

You can name the parameter instead of using it:

val name: String? = "Sam"
name?.let { nonNullName ->
    println("Name: $nonNullName")
    println("Uppercase: ${nonNullName.uppercase()}")
    println("Length: ${nonNullName.length}")
}

Inside the let block, the value is guaranteed to be non-null. This is safer than doing a null check with if.

Smart Casts

After a null check, Kotlin automatically “smart casts” the variable to a non-null type:

val name: String? = "Alex"

if (name != null) {
    // Kotlin knows name is non-null here — no ?. needed
    println("Name length: ${name.length}")
    println("Uppercase: ${name.uppercase()}")
}

Smart casts also work with type checks:

val value: Any? = "Hello"

when (value) {
    is String -> println("String with length ${value.length}")
    is Int -> println("Int with value $value")
    null -> println("It's null")
    else -> println("Unknown type")
}

After is String, Kotlin smart casts value to String, so you can call .length without any cast.

Safe Casting: as?

Regular casting with as crashes if the type is wrong:

val value: Any = "Hello"
val number: Int = value as Int // CRASH: ClassCastException

Safe casting with as? returns null if the cast fails:

val value: Any = "Hello"

val number: Int? = value as? Int
println(number) // null (no crash)

val str: String? = value as? String
println(str) // Hello

Combine with Elvis for a safe pattern:

val value: Any = "Hello"
val length = (value as? String)?.length ?: 0
println(length) // 5

lateinit

Sometimes you cannot initialize a variable right away, but you know it will be initialized before you use it. Use lateinit for this:

class UserProfile {
    lateinit var name: String
    lateinit var email: String

    fun initialize(name: String, email: String) {
        this.name = name
        this.email = email
    }

    fun printProfile() {
        if (::name.isInitialized) {
            println("Name: $name, Email: $email")
        } else {
            println("Profile not initialized yet")
        }
    }
}

Rules for lateinit:

  • Only works with var, not val
  • Only works with non-primitive types (not Int, Boolean, etc.)
  • Must be initialized before first use, or you get UninitializedPropertyAccessException
  • Use ::property.isInitialized to check if it was initialized
val profile = UserProfile()
profile.printProfile() // Profile not initialized yet

profile.initialize("Alex", "alex@example.com")
profile.printProfile() // Name: Alex, Email: alex@example.com

Practical Patterns

Pattern 1: Find or Default

val users = listOf("Alex", "Sam", "Jordan")
val found = users.find { it == "Sam" } ?: "Not found"
val notFound = users.find { it == "Taylor" } ?: "Not found"

println(found)    // Sam
println(notFound) // Not found

Pattern 2: Chain of Safe Calls

data class Address(val city: String?, val country: String?)
data class Person(val name: String, val address: Address?)

val person = Person("Alex", Address("Berlin", "Germany"))
val noAddress = Person("Sam", null)

println(person.address?.city?.uppercase())    // BERLIN
println(noAddress.address?.city?.uppercase()) // null

Pattern 3: Default Values for Nullable Properties

val city = person.address?.city ?: "Unknown city"
val city2 = noAddress.address?.city ?: "Unknown city"

println(city)  // Berlin
println(city2) // Unknown city

Pattern 4: Filter Out Nulls

val list: List<String?> = listOf("Alex", null, "Sam", null, "Jordan")
val nonNullList: List<String> = list.filterNotNull()

println(nonNullList) // [Alex, Sam, Jordan]

Pattern 5: Map with Nullable Values

val scores: Map<String, Int?> = mapOf(
    "Alex" to 95,
    "Sam" to null,
    "Jordan" to 87
)

for ((name, score) in scores) {
    println("$name: ${score ?: "No score"}")
}
// Alex: 95
// Sam: No score
// Jordan: 87

Null Safety Cheat Sheet

OperatorNameWhat It Does
?Nullable typeAllows null: String?
?.Safe callCalls method if not null, returns null otherwise
?:ElvisProvides default value if null
!!Not-null assertionConverts to non-null, crashes if null
as?Safe castReturns null if cast fails
?.let {}Null-safe letRuns block only if not null

Rules for Null Safety

  1. Use val and non-null types by default. Only use nullable types when you truly need them.
  2. Use ?. and ?: for null handling. They are safe and readable.
  3. Avoid !! in production code. It is a sign that you should refactor.
  4. Use let for complex null handling. It keeps the non-null value in scope.
  5. Use lateinit only when necessary. Prefer initializing in the constructor.

Source Code

You can find the complete source code for this tutorial on GitHub:

KT-4 Source Code on GitHub

What’s Next?

In the next tutorial, Kotlin Tutorial #5: Functions, Default Parameters, and Named Arguments, you will learn:

  • How to write functions in Kotlin
  • Single-expression functions
  • Default parameters
  • Named arguments
  • Varargs
  • The Unit return type

Functions in Kotlin are more powerful than in Java. See you there.


This is part 4 of the Kotlin Tutorial series. Check out Part 3: Variables and Types if you missed it. Need a quick reference? See the Kotlin Cheat Sheet.