Free Handbook · Every example compiled & verified

Null Safety

Nullable types, ?., ?:, !!, smart casts, as?, let, lateinit and lazy, Java platform types, and the NullPointerExceptions Kotlin still lets through.

0 / 136 lessons🔥 0 day streak
ShareXLinkedIn

Module 04 · what you'll be able to do

  • Read String and String? as different types, and explain what the compiler stops you doing with a nullable value
  • Handle null with ?., ?:, ?.let and smart casts instead of !!
  • Cast safely with as? and explain when a smart cast is impossible
  • Choose between lateinit, lazy and a nullable property for a value that is not ready at construction
  • Name the five ways a Kotlin program can still throw NullPointerException, including Java platform types
01

Nullable and non-null types

In Java, any object reference can be null, and calling a method on one crashes with a NullPointerException — the most common production bug on the JVM. Kotlin splits every type in two. String can never hold null. String? (with a question mark) can hold a string or null. The compiler then refuses to let you call .length on a String? until you have dealt with the null case. The crash moves from runtime, in front of a user, to compile time, in front of you.

kotlinMain.kt
fun main() {
    val name: String = "Asha"        // never null
    var nickname: String? = null      // may be null

    println(name.length)
    println(nickname)

    nickname = "Ash"
    if (nickname != null) {
        println(nickname.length)      // allowed: checked just above
    }

    val middle: String? = null
    println("middle is null? ${middle == null}")
}
Outputcompiled & run with real Kotlin
4
null
3
middle is null? true
Your turn

Declare val age: Int? = "31".toIntOrNull() and print age + 1. Read the compiler error, then fix it with an if check.

Error you will hit

Assigning null to a non-null type

kotlin
fun main() {
    val name: String = null
    println(name)
}
Main.kt:2:24: error: null cannot be a value of a non-null type 'String'.
Why the compiler said that

String without a question mark is a promise that the value is always a real string. null breaks that promise, so the file does not compile.

The fix

If "no value" is a legitimate state, declare the type as String?. If it is not, give it a real value.

kotlin
fun main() {
    val name: String? = null
    println(name)
}
Error you will hit

Calling a member on a nullable value

kotlin
fun main() {
    val name: String? = null
    println(name.length)
}
Main.kt:3:17: error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'String?'.
Why the compiler said that

This is the check that makes Kotlin null-safe. name might be null, and .length on null would crash, so the compiler insists you say what should happen in that case.

The fix

Use a safe call (name?.length, which gives null when name is null), add a default with ?:, or check with if (name != null) first. The next lessons cover all three.

kotlin
fun main() {
    val name: String? = null
    println(name?.length ?: 0)
}
02

Safe calls ?. and the Elvis operator ?:

The safe call a?.b evaluates to a.b when a is not null, and to null otherwise — without calling anything. Safe calls chain: user?.address?.city is null if any link is null. The Elvis operator a ?: b gives a if it is not null, otherwise b. Because return and throw are expressions in Kotlin, the right side can also leave the function: val id = input ?: return.

kotlinMain.kt
class Address(val city: String?)
class User(val name: String, val address: Address?)

fun cityOf(user: User?): String = user?.address?.city ?: "unknown"

fun greetingLength(name: String?): Int {
    val n = name ?: return -1          // leave early when null
    return n.length                    // n is a plain String here
}

fun main() {
    val full = User("Asha", Address("Pune"))
    val noCity = User("Ravi", Address(null))
    val noAddress = User("Meera", null)

    println(cityOf(full))
    println(cityOf(noCity))
    println(cityOf(noAddress))
    println(cityOf(null))

    println(full.address?.city?.uppercase())
    println(noAddress.address?.city?.uppercase())

    println(greetingLength("hello"))
    println(greetingLength(null))
}
Outputcompiled & run with real Kotlin
Pune
unknown
unknown
unknown
PUNE
null
5
-1
Your turn

Write fun requireCity(user: User): String that uses ?: throw IllegalStateException("no city"), and call it on full.

Read ?: as "or else"
val port = config.port ?: 8080 reads as "the configured port, or else 8080". It is Kotlin's answer to the ternary most Java code uses for null defaults, and it composes with safe calls: text?.trim()?.ifEmpty { null } ?: "blank".
03

Smart casts and safe casts with as?

After you check x != null or x is String, the compiler remembers the check and treats x as the narrower type in the code that follows — a smart cast. It works for local vals and vars nothing else can change, and for val properties in the same module. For an explicit cast, as throws ClassCastException when the value is the wrong type, while the safe cast as? returns null instead — which pairs naturally with ?:.

kotlinMain.kt
fun describe(value: Any?): String {
    if (value == null) return "nothing"
    // value is now Any (non-null)
    if (value is String) return "string of ${value.length}"   // smart cast to String
    if (value !is Int) return "some ${value::class.simpleName}"
    return "int doubled ${value * 2}"                          // smart cast to Int
}

fun main() {
    println(describe(null))
    println(describe("hey"))
    println(describe(21))
    println(describe(2.5))

    val raw: Any = "not a number"
    val n: Int? = raw as? Int
    println(n)
    println((raw as? String)?.uppercase())
    println((raw as? Int) ?: 0)
}
Outputcompiled & run with real Kotlin
nothing
string of 3
int doubled 42
some Double
null
NOT A NUMBER
0
Your turn

Pass listOf(1, 2) to describe and predict what simpleName prints.

Error you will hit

Smart cast impossible on a var property

kotlin
class User {
    var nickname: String? = "ash"
}

fun main() {
    val user = User()
    if (user.nickname != null) {
        println(user.nickname.length)
    }
}
Main.kt:8:17: error: smart cast to 'String' is impossible, because 'nickname' is a mutable property that could be mutated concurrently.
Why the compiler said that

nickname is a var on an object. Between the check and the use, another thread (or a function called in between) could set it back to null, so the compiler cannot trust the check.

The fix

Copy the property into a local val and check that — nothing can change a local val. user.nickname?.let { … } does the same thing in one step.

kotlin
class User {
    var nickname: String? = "ash"
}

fun main() {
    val user = User()
    val nick = user.nickname
    if (nick != null) {
        println(nick.length)
    }
}
Error you will hit

ClassCastException from an unsafe as

kotlin
fun main() {
    val value: Any = "42"
    val n = value as Int
    println(n + 1)
}
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
	at MainKt.main(Main.kt:3)
	at MainKt.main(Main.kt)
Why the compiler said that

as tells the compiler "trust me". It compiles, and at runtime the JVM finds a String where you promised an Int. A cast never converts: the text "42" does not become the number 42.

The fix

Use as? with a fallback when the type is uncertain, or convert text with toIntOrNull() when you actually want parsing.

kotlin
fun main() {
    val value: Any = "42"
    val n = (value as? String)?.toIntOrNull() ?: 0
    println(n + 1)
}
04

?.let, takeIf and friends

x?.let { … } runs the block only when x is not null, with the non-null value as it. It is the idiomatic way to act on a nullable property (it reads the value once, so the smart-cast problem above cannot happen) and to transform a nullable value in a chain. takeIf { condition } goes the other way: it turns a value into null when the condition fails, so a validation step can feed into ?:. The standard library has nullable-aware helpers too: isNullOrEmpty(), isNullOrBlank(), orEmpty().

kotlinMain.kt
fun sendEmail(to: String) = println("sending to $to")

fun main() {
    val emails: List<String?> = listOf("[email protected]", null, "  ", "[email protected]")

    for (email in emails) {
        email?.let { sendEmail(it) }                 // skips null
    }

    val clean = emails.map { it?.trim()?.takeIf { t -> t.contains("@") } }
    println(clean)

    val first: String? = null
    println(first.isNullOrBlank())
    println(first.orEmpty().length)

    val port = "80a".toIntOrNull()?.takeIf { it in 1..65535 } ?: 8080
    println(port)
}
Outputcompiled & run with real Kotlin
sending to [email protected]
sending to   
sending to [email protected]
[[email protected], null, null, [email protected]]
true
0
8080

The blank string " " is not null, so ?.let happily sends to it — the second loop shows the fix: trim, then takeIf to reject it.

Your turn

Change the loop to send only to addresses that survive the clean step.

Do not chain let for everything
a?.let { b(it) }?.let { c(it) }?.let { … } is hard to review. Two ?.lets is usually the limit; beyond that, an early ?: return per value and plain code reads better.
05

lateinit and lazy

Sometimes a property cannot get its value in the constructor — a test sets it in a setup method, a framework injects it, an Android view is bound in onCreate. Making it nullable would force ?. everywhere. lateinit var says "non-null, but assigned later"; reading it before assignment throws a clear exception, and ::prop.isInitialized checks first. It only works for var properties of non-primitive, non-null types. val x by lazy { … } is for the other case: a read-only value that is expensive to compute, calculated on first access and cached after that.

kotlinMain.kt
class Report {
    lateinit var title: String

    val summary: String by lazy {
        println("(computing summary once)")
        "Report: $title"
    }

    fun ready() = ::title.isInitialized
}

fun main() {
    val r = Report()
    println("ready? ${r.ready()}")
    r.title = "Q3 sales"
    println("ready? ${r.ready()}")

    println(r.summary)
    println(r.summary)   // cached, block does not run again
}
Outputcompiled & run with real Kotlin
ready? false
ready? true
(computing summary once)
Report: Q3 sales
Report: Q3 sales
Your turn

Move println(r.summary) above the r.title = … line. What exception do you get, and from which property?

Error you will hit

Reading lateinit before it is set

kotlin
class Screen {
    lateinit var title: String

    fun show() = println(title.uppercase())
}

fun main() {
    Screen().show()
}
Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property title has not been initialized
	at Screen.getTitle(Main.kt:2)
	at Screen.show(Main.kt:4)
	at MainKt.main(Main.kt:8)
	at MainKt.main(Main.kt)
Why the compiler said that

lateinit moves the null check from compile time to runtime. Nobody assigned title, so the first read fails. The message names the property, which makes it far easier to debug than a plain NPE.

The fix

Assign it before use (constructor, setup method, dependency injection). If the value can truly be missing, it should be a nullable property, not lateinit.

kotlin
class Screen {
    lateinit var title: String

    fun show() = println(title.uppercase())
}

fun main() {
    val s = Screen()
    s.title = "home"
    s.show()
}
ToolUse whenCost of misuse
String?Missing is a legitimate, expected stateNone — the compiler makes you handle it
lateinit varAlways set before use, but not in the constructor (DI, tests, Android views)UninitializedPropertyAccessException
val … by lazyExpensive, read-only, computed on first useExceptions surface at first access, not at construction
Constructor parameterThe value is known when the object is createdNone — prefer this whenever possible
06

Platform types and the NPEs you can still get

Java has no ? in its types, so when Kotlin calls a Java method it cannot know whether the result may be null. It gives the result a platform type, shown in IDE hints as String!: you may treat it as String or String?, and the compiler takes your word for it. Declare it String? and you are safe. Declare it String and Kotlin inserts a runtime check that fails immediately if Java hands back null — better than failing ten calls later, but still a crash.

kotlinMain.kt
fun main() {
    // System.getProperty is a Java method: its result is a platform type
    val home: String? = System.getProperty("app.home")   // not set in this run
    println(home ?: "app.home is not set")

    val javaMap = java.util.HashMap<String, Int>()
    javaMap["apples"] = 3
    val pears: Int? = javaMap["pears"]                  // Java returns null for a missing key
    println(pears)
    println(javaMap.getOrDefault("pears", 0))
}
Outputcompiled & run with real Kotlin
app.home is not set
null
0

Declaring Java results as nullable costs nothing and turns a possible crash into a value you handle.

Error you will hit

A null from Java assigned to a non-null type

kotlin
fun main() {
    val home: String = System.getProperty("app.home")
    println(home.length)
}
Exception in thread "main" java.lang.NullPointerException: getProperty(...) must not be null
	at MainKt.main(Main.kt:2)
	at MainKt.main(Main.kt)
Why the compiler said that

The compiler accepted String because the Java signature does not say otherwise. At runtime, Kotlin's inserted check caught the null on the assignment line and named the Java call that produced it.

The fix

Treat Java results that can be null as nullable, and supply a default or fail with a useful message.

kotlin
fun main() {
    val home: String = System.getProperty("app.home") ?: "/opt/app"
    println(home.length)
}
Error you will hit

The !! operator on a null

kotlin
fun main() {
    val email: String? = null
    println(email!!.length)
}
Exception in thread "main" java.lang.NullPointerException
	at MainKt.main(Main.kt:3)
	at MainKt.main(Main.kt)
Why the compiler said that

!! is the not-null assertion: it converts String? to String by promising the value is not null, and throws when the promise is false. It switches null safety off for that expression.

The fix

Replace !! with ?., ?: or a check. If null really is impossible, fail with a message that explains why: requireNotNull(email) { "email must be set before sending" }.

kotlin
fun main() {
    val email: String? = null
    println(email?.length ?: 0)
}

One more source surprises people: initialization order. A superclass constructor runs before the subclass's properties are assigned. If it calls an open function that the subclass overrides, that override sees its own non-null properties still holding null.

kotlinMain.kt
open class Base {
    init {
        println("Base init sees: ${describe()}")
    }
    open fun describe() = "base"
}

class Child : Base() {
    val name: String = "child"          // declared non-null
    override fun describe() = "name=$name"
}

fun main() {
    val c = Child()
    println("After construction: ${c.describe()}")
}
Outputcompiled & run with real Kotlin
Base init sees: name=null
After construction: name=child

A non-null String printed as null. Calling name.length in describe() would throw NullPointerException. Never call open members from a constructor or init block.

  • !! on a null value.
  • Java interop: a platform type assigned to a non-null type, or a Java method that receives null and dereferences it.
  • lateinit read before assignment (a clearer exception, same idea).
  • Initialization order: an open member called from a superclass constructor, or an init block that reads a property declared below it.
  • An explicit throw NullPointerException() — in your code or a library.
07

Collections of nullables

The question mark can go in two places, and they mean different things. List<String?> is a list that always exists but may contain nulls. List<String>? is a list of real strings that may itself be missing. Many collection functions return nullable results because the answer may not exist: firstOrNull(), getOrNull(i), maxOrNull(), and map[key] for a key that is not present. filterNotNull() removes the nulls and changes the type to List<String>; mapNotNull { } maps and drops nulls in one pass.

kotlinMain.kt
fun main() {
    val raw: List<String?> = listOf("12", null, "7", "x", null, "30")

    val present: List<String> = raw.filterNotNull()
    println(present)

    val numbers: List<Int> = raw.mapNotNull { it?.toIntOrNull() }
    println(numbers)
    println(numbers.sum())

    println(numbers.firstOrNull { it > 100 })
    println(numbers.getOrNull(10))
    println(emptyList<Int>().maxOrNull())

    val prices = mapOf("tea" to 20, "coffee" to 35)
    val juice: Int? = prices["juice"]
    println(juice ?: "not on the menu")
    println(prices.getOrElse("juice") { 0 })

    val maybeList: List<String>? = null
    println(maybeList?.size ?: 0)
    println(maybeList.orEmpty().size)
}
Outputcompiled & run with real Kotlin
[12, 7, x, 30]
[12, 7, 30]
49
null
null
null
not on the menu
0
0
0
Your turn

Count the nulls in raw with raw.count { it == null }, and find the longest present string with maxByOrNull.

Nullable type
A type with ?, such as String?, that can hold a value or null.
Safe call ?.
a?.b evaluates to null instead of calling b when a is null.
Elvis operator ?:
a ?: b gives a if it is not null, otherwise b. The right side can be return or throw.
Not-null assertion !!
Converts a nullable value to non-null, throwing NullPointerException if it is null.
Smart cast
The compiler treating a value as a narrower type after a null check or is check.
Safe cast as?
A cast that returns null instead of throwing ClassCastException.
lateinit
Marks a non-null var that is assigned after construction; reading it early throws UninitializedPropertyAccessException.
lazy
A delegate that computes a val on first access and caches the result.
Platform type
The type of a value coming from Java (shown as String!), whose nullability Kotlin cannot know.
filterNotNull()
Returns the non-null elements of a collection, typed as non-null.
Quick check

What does val s: String? = null; println(s?.length ?: -1) print?

Quick check

Which declaration can hold listOf("a", null)?

Frequently asked questions

Can Kotlin code still throw NullPointerException?
Yes, in a few specific cases: the !! operator on a null, Java platform types assigned to non-null Kotlin types, reading a lateinit property too early (a dedicated exception), initialization-order problems when a constructor calls an open member, and explicit throws. Pure Kotlin code that avoids !! essentially never sees one.
When should I use !! in Kotlin?
Almost never. Each !! is a place where you told the compiler to stop checking. Prefer ?., ?:, ?.let or an early return. If a null truly indicates a bug, requireNotNull(x) { "reason" } or checkNotNull fails with a message instead of a bare NPE.
What is the difference between lateinit and lazy?
lateinit var is a mutable property that something outside assigns later, such as a test setup or a dependency injector. val … by lazy { } is a read-only property whose own block computes the value on first access. Use lazy for expensive derived values, lateinit for values injected from outside.

Finish the Kotlin handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.