Skip to content

Kotlin Null Safety — Complete Guide with Examples

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Kotlin Null Safety. We cover key concepts, practical examples, and best practices to help you master this topic.

Kotlin null safety distinguishes nullable and non-nullable types at the compiler level, using safe calls (?.), the Elvis operator (?:), the not-null assertion (!!), and smart casts to eliminate NullPointerException from production code.

What You'll Learn

  • Declare nullable and non-nullable types
  • Use safe calls (?.) for chained access
  • Apply the Elvis operator (?:) for default values
  • Understand the not-null assertion (!!) and its risks
  • Use smart casts after null checks
  • Work with the let function for null-safe execution
  • Handle platform types when calling Java code

Why It Matters

NullPointerException is the most common crash in Java applications. Studies show it accounts for 20-30% of all production crashes. Kotlin solves this at the language level by making null a type-system concept. Non-nullable types guarantee that a variable will never be null. The compiler enforces this, eliminating entire categories of bugs. For Android developers, this means fewer ANRs and crashes. For server-side developers, it means more reliable services.

Real-World Use

DodaTech's Android utility app reduced crash rate by 40% after migrating from Java to Kotlin, primarily due to null safety. Backend API responses often contain optional fields. Kotlin's nullable types model these naturally, and safe calls prevent crashes when optional data is missing.

Learning Path

flowchart LR
  A[Functions] --> B[Null Safety\nYou are here]
  B --> C[Collections]
  style B fill:#f90,color:#fff

Nullable versus Non-Nullable Types

By default, Kotlin types cannot hold null. Add ? to make a type nullable.

fun main() {
    // Non-nullable: cannot be null
    var name: String = "Alice"
    // name = null  // Compilation error
    
    // Nullable: can be null
    var nullableName: String? = "Bob"
    nullableName = null  // OK
    
    // Non-nullable function parameter
    fun printLength(text: String) {
        println(text.length)
    }
    
    // printLength(null)  // Compilation error
    
    // Nullable function parameter
    fun printNullableLength(text: String?) {
        println(text?.length ?: 0)
    }
    
    printNullableLength(null)      // Output: 0
    printNullableLength("Hello")   // Output: 5
}

Output: The non-nullable parameter rejects null at compile time. The nullable parameter handles both null and non-null values safely.

Safe Calls (?.)

The safe call operator accesses a property or method only if the receiver is non-null. Otherwise, it returns null.

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

fun main() {
    val person1: Person? = Person("Alice", Address("123 Main St", null))
    val person2: Person? = null
    
    // Chained safe calls
    val city1 = person1?.address?.city
    val city2 = person2?.address?.city
    
    println(city1)  // Output: null (city is null)
    println(city2)  // Output: null (person is null)
    
    // Safe call with function invocation
    val length = person1?.name?.length
    println(length)  // Output: 5
    
    // Safe call on nullable list
    val numbers: List<Int>? = listOf(1, 2, 3)
    val size = numbers?.size
    println(size)  // Output: 3
}

Output: The safe call chain returns null if any intermediate value is null, without throwing an exception.

Elvis Operator (?:)

The Elvis operator provides a default value when the expression on the left is null.

fun main() {
    val name: String? = null
    val displayName = name ?: "Guest"
    println(displayName)  // Output: Guest
    
    val actualName: String? = "Alice"
    val displayActual = actualName ?: "Guest"
    println(displayActual)  // Output: Alice
    
    // Elvis with early return
    fun getUserEmail(userId: Int): String? {
        // Simulated lookup
        return if (userId == 1) "alice@example.com" else null
    }
    
    val email = getUserEmail(2) ?: return
    println(email)  // This line never executes because return runs first
}

Output: The Elvis operator substitutes "Guest" when the name is null. It returns the original value otherwise.

The Elvis operator can also be used with return or throw:

fun processUser(user: User?) {
    val name = user?.name ?: throw IllegalArgumentException("User must have a name")
    println("Processing: $name")
}

Not-Null Assertion (!!)

The double-bang operator converts any nullable type to a non-nullable type, throwing NullPointerException if the value is null.

fun main() {
    var text: String? = "Hello"
    val length = text!!.length
    println(length)  // Output: 5
    
    text = null
    // val crash = text!!.length  // Throws NullPointerException
    
    // Practical use: when you know a value is non-null
    val numbers: List<Int>? = listOf(1, 2, 3)
    val first = numbers!!.first()
    println(first)  // Output: 1
}

Output: The not-null assertion works when the value is non-null. Using it on a null value crashes the program.

The !! operator is a code smell. Prefer safe calls, Elvis, or let instead. Use !! only when you are certain the value is non-null and the compiler cannot infer it.

Smart Casts

Kotlin automatically casts nullable types to non-nullable after a null check.

fun describeString(text: String?) {
    if (text != null) {
        // Smart cast: text is now String (non-nullable)
        println("Length: ${text.length}, Uppercase: ${text.uppercase()}")
    } else {
        println("Text is null")
    }
}

fun main() {
    describeString("Kotlin")  // Output: Length: 6, Uppercase: KOTLIN
    describeString(null)      // Output: Text is null
    
    // Smart cast with when
    val value: Any? = "Hello"
    when (value) {
        is String -> println("String of length ${value.length}")
        is Int -> println("Number: $value")
        null -> println("Is null")
        else -> println("Unknown type")
    }
    // Output: String of length 5
}

Output: After the null check, the compiler treats text as non-nullable String, allowing direct access to length and uppercase.

The let Function for Null Safety

Combine let with the safe call operator to execute code only on non-null values.

data class User(val id: Int, val name: String)

fun findUserById(id: Int): User? {
    return if (id == 1) User(1, "Alice") else null
}

fun main() {
    val user = findUserById(1)
    
    // Execute block only if user is not null
    user?.let {
        println("Found user: ${it.name} (ID: ${it.id})")
        // Multiple operations on non-null user
        val uppercaseName = it.name.uppercase()
        println("Uppercase: $uppercaseName")
    }
    // Output: Found user: Alice (ID: 1)
    //         Uppercase: ALICE
    
    val missingUser = findUserById(2)
    missingUser?.let {
        println("This will not print")
    }
    // No output: let block is skipped
}

Output: The let block executes only when the user is non-null. This is the idiomatic way to handle nullable values in Kotlin.

Platform Types

When calling Java code from Kotlin, Java types become platform types. The compiler does not enforce null safety on them.

// Java class
// public class JavaUser {
//     private String name;
//     public String getName() { return name; }
// }

// Kotlin call site
fun processJavaUser(javaUser: JavaUser?) {
    // Platform type: String! (nullable or non-nullable unknown)
    val name = javaUser?.name
    
    // You decide the nullability
    val safeName: String? = name
    val unsafeName: String = name ?: "Unknown"
}

Output: Platform types have a ! suffix in error messages. They require explicit null handling at the Kotlin boundary.

Use nullable types for Java values that might be null. Add @Nullable and @NotNull annotations to Java code for better interop.

Common Mistakes

  1. Overusing !! everywhere: Using !! defeats null safety and reintroduces NPE risk. Reserve it for rare cases where you are absolutely certain the value is non-null.

  2. Ignoring the Elvis operator: Instead of writing if (x != null) x else defaultValue, use x ?: defaultValue. It is shorter and more idiomatic.

  3. Forgetting to handle platform types: When calling Java APIs, assume every return value might be null unless annotated otherwise.

  4. Using safe calls when null is not valid: If a null value indicates a bug, fail early with requireNotNull or checkNotNull rather than silently propagating null.

  5. Nesting let calls: Multiple nested let calls create deeply indented code. Use chained safe calls or extract logic into separate functions.

  6. Smart cast limitations with mutable properties: Smart casts work only on val properties that are not subject to change between check and use. For var properties, use safe calls or let.

Practice Questions

  1. What is the difference between String and String? in Kotlin?

Answer: String is a non-nullable type that cannot hold null. String? is a nullable type that can hold either a String value or null.

  1. When should you use the !! operator?

Answer: Only when you are certain a nullable value is non-null at the point of access and the compiler cannot verify it. Typical cases include values checked earlier in the function or values from well-known APIs.

  1. What does the Elvis operator (?:) do?

Answer: It returns the left-hand value if it is non-null, otherwise it returns the right-hand value. It provides a default for nullable expressions.

  1. How does a smart cast work?

Answer: After a null check or type check, the compiler automatically casts the variable to the checked type, eliminating the need for explicit casts.

  1. Challenge: Write a function that takes a list of nullable strings, filters out null values, converts the remaining strings to uppercase, and returns the result as a single comma-separated string. Handle the case where the list itself is null.

Answer:

fun processStrings(strings: List<String?>?): String {
    return strings
        ?.filterNotNull()
        ?.map { it.uppercase() }
        ?.joinToString(", ")
        ?: "List was null"
}

fun main() {
    println(processStrings(listOf("apple", null, "banana", null, "cherry")))
    // Output: APPLE, BANANA, CHERRY
    println(processStrings(null))
    // Output: List was null
}

Mini Project

Create a safe configuration loader that reads environment variables and provides typed access with null safety. Requirements:

  • Define a Config class that wraps a Map<String, String?>
  • Implement getString(key, default) with Elvis operator
  • Implement getInt(key, default) with safe conversion
  • Implement getRequiredString(key) that throws on null
  • Implement getStringOrNull(key) using safe access
  • Use let for executing actions only when a config value exists
  • Write tests that cover null, missing, and present keys

This project applies all null safety mechanisms in a practical, real-world pattern.

FAQ

Does Kotlin eliminate all NullPointerExceptions?

Kotlin eliminates NPEs caused by uninitialized nullable types. NPEs can still occur from using !! on null values, platform types from Java, and JVM internal errors.

What is a platform type?

A platform type is a type coming from Java whose nullability is unknown. It is denoted with a single ! suffix in error messages. You must handle it as potentially nullable.

Can I use safe calls with method calls?

Yes. person?.getName() calls getName only if person is non-null. The result type is nullable (String? in this case).

What is the difference between ?. and ?:?

?. (safe call) accesses a member only if the receiver is non-null. ?: (Elvis) provides a default value when the left side is null. They are often used together.

How does Kotlin handle null in collections?

A List<String?> can contain null elements. Use filterNotNull() to remove nulls. A List? means the list reference itself can be null.

What's Next

Now that you understand null safety, learn collections for working with lists, sets, and maps. You can also explore lambdas to write functional-style collection operations.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro