Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Kotlin  Module 3 – Null safety

Nullable types, safe calls and Elvis

Declare nullable types in Kotlin, read them with safe calls and the Elvis operator, return early for a missing value, and see what not-null assertions throw.

  • Beginner
  • 10 minutes
  • Examples run with Kotlin 2.4.20
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Declare nullable types and predict which uses of them the compiler refuses
  • Read nullable values with safe-call chains and let, and predict where a chain stops
  • Use the Elvis operator to supply a fallback, return early or throw when a value is null
  • Explain what !! throws and replace it with a fallback, an early return or requireNotNull
  • Test a nullable Boolean in a condition with == true or ?: false

Before you start

What you’ll learn · Before you start

What you will learn

  • Declare nullable types and predict which uses of them the compiler refuses
  • Read nullable values with safe-call chains and let, and predict where a chain stops
  • Use the Elvis operator to supply a fallback, return early or throw when a value is null
  • Explain what !! throws and replace it with a fallback, an early return or requireNotNull
  • Test a nullable Boolean in a condition with == true or ?: false

Before you start

On this page

A train booking can be missing information at three levels: the PNR may not exist, a waitlisted booking has no coach yet, and a coach may not have a berth for this passenger. Code that reads booking.coach.berth.number as if all three were always there is the classic way a Java program ends in a NullPointerException.

Kotlin writes the gap into the type. A String always holds a string, a String? may hold null, and the compiler refuses code that treats the second like the first. You will declare both kinds of type, read nullable values with safe calls and let, use the Elvis operator ?: for fallbacks and early returns, and see exactly what the not-null assertion !! throws when you use it anyway.

String and String? are different types

A booking form asks every passenger for a name and lets them leave the nickname empty. Declare the name as a String and the nickname as a String?: the question mark makes a nullable type: A type written with ? after it, such as String?, whose values are those of the type plus null. The compiler refuses a plain call on it until the code says what happens when the value is null., one whose values are the strings plus null.

Types.kt: a name that is always there, and a nickname that may not be Kotlin · types/Types.kt
// A booking form: every passenger has a name, and a nickname only if they typed one.
fun main() {
    val name: String = "Asha"
    println("name: $name, ${name.length} letters")

    // Two passengers: one typed a nickname, the other left the box empty.
    for (nickname in listOf<String?>("Ash", null)) {
        println("nickname: $nickname, is it null? ${nickname == null}")
    }

    // Every String is also a String?, so this assignment needs no check.
    val shown: String? = name
    println("shown: $shown")
}

Output

name: Asha, 4 letters
nickname: Ash, is it null? false
nickname: null, is it null? true
shown: Asha

Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc Types.kt -include-runtime -d main.jar && java -jar main.jar

The loop gives nickname a string, then null, and both print. The last line shows the direction that always works: every String is also a String?, so a value moves from the non-null type to the nullable one without a check. The other direction is where the compiler stops you:

TypeErrors.kt: three lines the compiler refuses Kotlin · type-errors/TypeErrors.kt
// Three lines the compiler refuses: each one could let a null in where a String is promised.
fun main() {
    val nickname: String? = "Ash"
    val pnr: String = null
    val shown: String = nickname
    println(nickname.length)
}

Output (exit status 1)

Printed as an error (standard error)

TypeErrors.kt:4:23: error: null cannot be a value of a non-null type 'String'.
    val pnr: String = null
                      ^^^^
TypeErrors.kt:5:23: error: initializer type mismatch: expected 'String', actual 'String?'.
    val shown: String = nickname
                      ^
TypeErrors.kt:6:21: error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'String?'.
    println(nickname.length)
                    ^

Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc TypeErrors.kt -include-runtime -d main.jar && java -jar main.jar

Line 4 tries to put null into a String. Line 5 assigns a String? to a String and is refused although nickname holds “Ash” at that moment: the compiler goes by the declared type, not by the value the variable happens to hold. Line 6 calls length on a String?, and the message names the two calls that are allowed there, the safe call ?. and the not-null assertion !!., both covered below.

Behaviour String String?
Can hold null no yes
x.length compiles refused
Goes into the other type always only after a decision about null

All of this happens while compiling. When the program runs, a String? that holds a string is the same string object as any other; the Kotlin guide to nullability says the two kinds of type are treated the same at run time.

Checkpoint

Check: String and String?

  1. Question 1 of 2 nickname is declared as val nickname: String? = "Ash". Which of these lines compile?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • val shown: String? = "Asha"
    • val n: Int? = nickname?.length

    A nullable type adds null to the values of its type. A plain call on it is refused, a safe call is allowed, and a value can always move from String to String? but never back without a decision about null.

  2. Question 2 of 2 val nickname: String? = "Ash" is followed by val shown: String = nickname. What happens?

    Choose one answer.

    A hint

    Does the compiler look at the declaration or at the value?

    Show the answer: Question 2 of 2

    Answer: It does not compile: initializer type mismatch, expected String, actual String?

    Assigning a String? to a String needs a decision about null first: a safe call, the Elvis operator or a check. The value a variable happens to hold does not change its declared type.

A safe call gives null instead of throwing

A berth number sits three steps away from its PNR, and each step can be missing. bookings[pnr] looks the PNR up in a map, and a map lookup gives null for a key it does not hold, so its type is Booking?. A waitlisted booking has no coach, and a coach may have no berth for this passenger yet. The safe call: The ?. operator: a?.b reads b only when a is not null and gives null otherwise, so a chain of safe calls stops at its first null instead of throwing. ?. reads a member only when the value on its left is not null, and gives null when it is:

Berth.kt: one chain of safe calls, four PNRs Kotlin · berth/Berth.kt
// A booking can be missing at three levels: the PNR may be unknown, the booking may have no coach yet
// (waitlisted), and the coach may have no berth for this passenger yet.
class Berth(val number: Int, val kind: String)

class Coach(val code: String, val berth: Berth?)

class Booking(val passenger: String, val coach: Coach?)

val bookings: Map<String, Booking> = mapOf(
    "8240117365" to Booking("Asha", Coach("B2", Berth(34, "lower"))),
    "8240117366" to Booking("Ravi", Coach("S5", null)),
    "8240117367" to Booking("Meena", null),
)

// bookings[pnr] is a Booking? (null for an unknown PNR), so every step after it is a safe call.
fun berthNumber(pnr: String): Int? = bookings[pnr]?.coach?.berth?.number

fun main() {
    for (pnr in listOf("8240117365", "8240117366", "8240117367", "8240117399")) {
        println("$pnr -> ${berthNumber(pnr)}")
    }
}

Output

8240117365 -> 34
8240117366 -> null
8240117367 -> null
8240117399 -> null

Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc Berth.kt -include-runtime -d main.jar && java -jar main.jar

Only Asha’s booking reaches a berth number. Step through the chain for Meena, whose booking is waitlisted, then for Asha, for Ravi and for a PNR that is not in the map:

Interactive · stage by stage Where bookings[pnr]?.coach?.berth?.number stops Step through the chain for Meena's waitlisted booking, then for Asha, Ravi and a PNR that is not in the map. Each ?. looks at the value on its left first.
Where bookings[pnr]?.coach?.berth?.number stops, step 8 of 8. Showing bookings[pnr] (null: not in the map), ?.coach (never read), ?.berth (never read), ?.number (never read), Value of the chain (null). In focus: bookings[pnr], stop-lookup.null: stopnull: stopnull: stopbookings[pnr]null: not in the map?.coachnever read?.berthnever read?.numbernever readValue of the chainnull

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Meena's PNR. For Meena's PNR, 8240117367, the map lookup finds a booking. The value on the left of the first ?. is not null, so the chain goes on to read the coach.
  2. No coach yet. Meena is waitlisted, so her booking has no coach yet, and ?.coach gives null.
  3. The chain stops. The next ?. finds null on its left, so berth and number are never read and the whole chain is null. Berth.kt printed 8240117367 -> null.
  4. Asha's PNR. Asha's PNR, 8240117365, starts again from the map lookup, which finds her booking.
  5. Every link is there. Her booking has coach B2, and the coach holds her berth, so each ?. finds a value on its left and the chain goes on.
  6. A number at the end. ?.number reads 34, the value of the whole chain. Its type is still Int?, because the compiler cannot know which bookings will be complete.
  7. Ravi's PNR. Ravi's booking, 8240117366, has coach S5 but no berth yet, so his chain gets one link further than Meena's and stops at ?.berth. It is null too.
  8. An unknown PNR. For 8240117399 the map has no entry, so bookings[pnr] is null and no member after it is read. Three of the four PNRs end in the same null, for three different reasons.

A chain stops at its first null: once ?.coach gives null, berth and number are never read, and the whole chain is null. Each safe call has a nullable result, so the next link needs ?. as well, and the chain as a whole is an Int?. That is also its weakness. Three of the four PNRs printed the same null, and a caller of berthNumber cannot tell an unknown PNR from a waitlisted booking.

Checkpoint

Check: Safe calls and where a chain stops

  1. Question 1 of 2 Ravi's booking, 8240117366, has coach S5 and no berth yet. What do bookings["8240117366"]?.coach?.code and bookings["8240117366"]?.coach?.berth?.number give?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: "S5", then null

    A chain of safe calls goes on while every link has a value and stops at the first null. Ravi's chain reaches the coach and stops at the berth.

  2. Question 2 of 2 number is an Int property of Berth. What is the type of bookings[pnr]?.coach?.berth?.number?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Int?

    a?.b has the type of b made nullable. In a chain, the last link decides the type, and the question mark comes from every link before it that could be null.

The Elvis operator supplies the value for null

x ?: y is the Elvis operator: The ?: operator: x ?: y gives x when x is not null and evaluates y only when x is null. The right side can be a fallback value, return or throw.. It gives x when x is not null, and works out y only when x is null. The right side can be a fallback value, and it can also be return or throw, which are expressions in Kotlin, so a function can stop at the first missing piece and say which one it was:

Elvis.kt: one answer for each missing piece, and a fallback that runs only when needed Kotlin · elvis/Elvis.kt
// The same bookings, now with a message for each way a berth can be missing.
class Berth(val number: Int, val kind: String)

class Coach(val code: String, val berth: Berth?)

class Booking(val passenger: String, val coach: Coach?)

val bookings: Map<String, Booking> = mapOf(
    "8240117365" to Booking("Asha", Coach("B2", Berth(34, "lower"))),
    "8240117366" to Booking("Ravi", Coach("S5", null)),
    "8240117367" to Booking("Meena", null),
)

// Each ?: return ends the function early, and after it the name on the left is not null.
fun berthLabel(pnr: String): String {
    val booking = bookings[pnr] ?: return "$pnr: no such booking"
    val coach = booking.coach ?: return "${booking.passenger}: waitlisted, no coach yet"
    val berth = coach.berth ?: return "${booking.passenger}: coach ${coach.code}, berth not allotted yet"
    return "${booking.passenger}: coach ${coach.code}, berth ${berth.number} ${berth.kind}"
}

fun passengerOf(pnr: String): String = bookings[pnr]?.passenger ?: throw IllegalArgumentException("unknown PNR $pnr")

fun noBerth(): String {
    println("  (noBerth() runs)")
    return "no berth"
}

fun main() {
    for (pnr in listOf("8240117365", "8240117366", "8240117367", "8240117399")) {
        println(berthLabel(pnr))
    }
    // The right side of ?: runs only when the left side is null.
    for (pnr in listOf("8240117365", "8240117367")) {
        println(bookings[pnr]?.coach?.berth?.kind ?: noBerth())
    }
    println(passengerOf("8240117366"))
    try {
        passengerOf("8240117399")
    } catch (e: IllegalArgumentException) {
        println("caught: ${e.message}")
    }
}

Output

Asha: coach B2, berth 34 lower
Ravi: coach S5, berth not allotted yet
Meena: waitlisted, no coach yet
8240117399: no such booking
lower
  (noBerth() runs)
no berth
Ravi
caught: unknown PNR 8240117399

Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc Elvis.kt -include-runtime -d main.jar && java -jar main.jar

The four PNRs now get four different answers. After the first line of berthLabel, booking has the type Booking, not Booking?: the line returns early whenever the lookup gives null, so the next line can read booking.coach with a plain dot. In the second loop, noBerth() ran once, for Meena, and not for Asha, whose berth was there: the right side is evaluated only when the left side is null. passengerOf ends with ?: throw, and its exception carries the PNR.

On the right of ?: Use it when In Elvis.kt
a value a missing value has a sensible default ?: noBerth()
return the function has nothing more to do without the value ?: return "$pnr: no such booking"
throw a missing value is a mistake to report ?: throw IllegalArgumentException(…)

Key idea

x ?: y gives x unless it is null, and runs y only then; with return or throw on the right, everything after the line can treat the name on the left as not null.

Checkpoint

Check: The Elvis operator, with return and throw

  1. One question In the second loop of Elvis.kt, bookings[pnr]?.coach?.berth?.kind ?: noBerth() runs for Asha and for Meena. How many times does noBerth() run?

    Type a number.

    A hint

    For whom is the left side null?

    Show the answer: One question

    Answer: 1 times

    Asha's chain gives "lower", so the right side is not evaluated. Meena's chain is null, so noBerth() runs once and prints its note, then gives "no berth". The recorded output shows the note once.

A null check or let runs code only when a value is there

Sometimes there is no fallback to give: you want to run some code when a value is there, and nothing when it is not. Two ways do that, an if that compares with null and ?.let:

LetCheck.kt: an if that checks for null, and ?.let Kotlin · let-check/LetCheck.kt
// Two ways to run code only when a value is there: an if that checks for null, and ?.let.
fun handleOf(nickname: String): String? = if (nickname == "Ash") null else "@$nickname"

fun main() {
    for (nickname in listOf("Ash", null)) {
        if (nickname != null) {
            // Inside the if, nickname is a String: no ? is needed for length.
            println("$nickname has ${nickname.length} letters")
        } else {
            println("no nickname")
        }
        // let runs its block only when nickname is not null; inside it, the value is called it.
        val greeting = nickname?.let { "Hello, $it!" }
        println("greeting: $greeting")

        // A trap: ?: also replaces a null that the block itself returns.
        val handle = nickname?.let { handleOf(it) } ?: "no nickname"
        println("handle: $handle")
    }
}

Output

Ash has 3 letters
greeting: Hello, Ash!
handle: no nickname
no nickname
greeting: null
handle: no nickname

Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc LetCheck.kt -include-runtime -d main.jar && java -jar main.jar

Inside if (nickname != null), the compiler treats nickname as a String, so nickname.length needs no ?. This is a smart cast: The compiler's own cast: after a check such as x != null or x is String, it treats x as the checked type wherever the check still holds, with no cast written. More in “Smart casts, is checks and safe casts”, the subject of the next lesson. nickname?.let { … } calls let only when nickname is not null; inside the block the value is called it and is a String, and the whole expression gives the block’s last value, or null when the block was skipped. That is why greeting printed null the second time.

Putting ?: after a let block that can return null

nickname?.let { handleOf(it) } ?: "no nickname" printed “no nickname” for Ash, who has a nickname: handleOf returned null, and ?: replaces every null on its left, whichever part made it. When the block can give null, write the two cases out with an if instead.

Prefer the if when there is an else to write, and ?.let for one short action or one transformation of a value. A ?.let nested inside another ?.let is harder to read than the early returns of berthLabel.

Checkpoint

Check: A null check, and ?.let

  1. One question In LetCheck.kt, nickname?.let { handleOf(it) } ?: "no nickname" printed "no nickname" for Ash, who has a nickname. Why?

    Choose one answer.

    Show the answer: One question

    Answer: handleOf returned null for Ash, and ?: replaces every null on its left, including one the block gives

    x?.let { f(it) } ?: fallback gives the fallback both when x is null and when f returns null. Write the two cases out with an if when the block itself can give null.

The not-null assertion throws when the value is missing

x!! is the not-null assertion: The !! operator: x!! gives x as a non-null type, and throws a NullPointerException without a message when x is null.: it gives x as a non-null type when x is not null, and throws a NullPointerException when it is. Here is the berth lookup written with it:

Bang.kt: the berth lookup with !! instead of safe calls Kotlin · bang/Bang.kt
// The berth lookup written with !! instead of safe calls.
class Berth(val number: Int, val kind: String)

class Coach(val code: String, val berth: Berth?)

class Booking(val passenger: String, val coach: Coach?)

val bookings: Map<String, Booking> = mapOf(
    "8240117365" to Booking("Asha", Coach("B2", Berth(34, "lower"))),
    "8240117367" to Booking("Meena", null),
)

fun berthNumber(pnr: String): Int = bookings[pnr]!!.coach!!.berth!!.number

fun main() {
    println("8240117365 -> ${berthNumber("8240117365")}")
    println("8240117367 -> ${berthNumber("8240117367")}")
}

Output (exit status 1)

8240117365 -> 34

Printed as an error (standard error)

Exception in thread "main" java.lang.NullPointerException
	at BangKt.berthNumber(Bang.kt:13)
	at BangKt.main(Bang.kt:17)
	at BangKt.main(Bang.kt)

Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc Bang.kt -include-runtime -d main.jar && java -jar main.jar

Asha’s lookup works. Meena’s stops the program with a NullPointerException that has no message at all, and the stack trace points at line 13, in berthNumber. That line holds three !!, so the trace cannot say which value was missing; here it was the coach, because Meena’s booking exists but is waitlisted. Every !! promises the compiler that a value is never null, and a broken promise costs a crash with no explanation.

When a missing value really is a mistake, say so with requireNotNull, which returns the value as a non-null type, or throws an IllegalArgumentException with your message:

Require.kt: requireNotNull with a message Kotlin · require/Require.kt
// requireNotNull gives the missing value a name before the program stops.
class Booking(val passenger: String, val coach: String?)

val bookings: Map<String, Booking> = mapOf(
    "8240117365" to Booking("Asha", "B2"),
    "8240117367" to Booking("Meena", null),
)

fun bookingFor(pnr: String): Booking = requireNotNull(bookings[pnr]) { "no booking for PNR $pnr" }

fun main() {
    println(bookingFor("8240117365").passenger)
    println(bookingFor("8240117367").coach ?: "no coach yet")
    println(bookingFor("8240117399").passenger)
}

Output (exit status 1)

Asha
no coach yet

Printed as an error (standard error)

Exception in thread "main" java.lang.IllegalArgumentException: no booking for PNR 8240117399
	at RequireKt.bookingFor(Require.kt:9)
	at RequireKt.main(Require.kt:14)
	at RequireKt.main(Require.kt)

Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc Require.kt -include-runtime -d main.jar && java -jar main.jar

The log now names the PNR. The two kinds of missing value need different tools, and two questions about the value decide which one fits. Answer them in the table:

Interactive · answer and compare What should the code do when this value is missing? Answer two questions about the value. The table marks the ways that fit and says why the others do not.
Is a missing value a normal case here, such as a waitlisted booking?
When the value is missing, what should this code do?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Five ways to handle a value that may be null, against three things the code needs
Way The program goes on Says what was missing The result is not null
Safe call Written as: a?.b, the result may be null Good the chain gives null and the code carries on Poor Berth.kt printed the same null for three different gaps Poor an Int?, so every caller has to decide again
Elvis with a value Written as: a ?: fallback Good the fallback takes the place of null Mixed only if the fallback says it, as "no berth" did Good the fallback gives a value of the non-null type
Elvis with return Written as: a ?: return … Mixed this function stops early; its caller carries on Good berthLabel gave the four PNRs four different answers Good after the line, the name on the left is not null
requireNotNull Written as: requireNotNull(a) { "…" } Poor throws IllegalArgumentException Good the message named PNR 8240117399 Good returns the value as a non-null type
Not-null assertion Written as: a!! Poor throws NullPointerException Poor no message, and line 13 of Bang.kt held three !! Good gives a non-null type when it does not throw
How the answers decide
  • “Is a missing value a normal case here, such as a waitlisted booking?” Yes, it happens in normal use: rules out requireNotNull and Not-null assertion. An exception for a normal case turns everyday data, such as a waitlisted booking, into a crash or an error in the log.
  • “Is a missing value a normal case here, such as a waitlisted booking?” No, it can only be a mistake: rules out Not-null assertion and adds a caveat to Safe call and Elvis with a value. A mistake should be reported with its cause: requireNotNull names it, !! throws without a message, and a fallback hides it.
  • “When the value is missing, what should this code do?” Use a default and go on: rules out Safe call and Elvis with return. A default belongs on the right of ?:, and a safe call on its own leaves the null for the caller.
  • “When the value is missing, what should this code do?” Stop this function with an answer: rules out Safe call and Elvis with a value. ?: return ends the function at the first missing value and can say which one it was, as berthLabel does.
  • “When the value is missing, what should this code do?” Give null back to the caller: rules out Elvis with a value, Elvis with return, requireNotNull and Not-null assertion. Only a safe call keeps the null, and its nullable result type tells the caller that the value may be missing.

In short: a safe call when the caller should decide, ?: with a value or return when a missing value is a normal case with an answer of its own, requireNotNull with a message when it is a mistake, and !! almost never.

Checkpoint

Check: !! and requireNotNull

  1. Question 1 of 2 Bang.kt stopped at line 13, bookings[pnr]!!.coach!!.berth!!.number, for Meena's PNR. Which of the three !! threw?

    Choose one answer.

    A hint

    Meena's booking is in the map, and she is waitlisted.

    Show the answer: Question 1 of 2

    Answer: The second, after coach, because her booking has no coach yet

    Each !! throws the same NullPointerException without a message. With several on one line, only the data tells you which value was missing.

  2. Question 2 of 2 What does requireNotNull(bookings[pnr]) { "no booking for PNR $pnr" } do for a PNR that is not in the map?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It throws an IllegalArgumentException with the message no booking for PNR and the PNR

    requireNotNull(x) { message } gives x as a non-null type when it is there, and otherwise throws IllegalArgumentException with the message, so the log says what was missing.

A nullable Boolean needs an explicit comparison

A form’s yes-or-no question has three answers once a passenger can skip it: true, false and null. An if needs a Boolean, so a Boolean? cannot be its condition:

BooleanError.kt: a Boolean? as the condition of an if Kotlin · boolean-error/BooleanError.kt
// A Boolean? cannot be the condition of an if: it has a third value, null.
fun main() {
    val senior: Boolean? = listOf(true, false, null).last()
    if (senior) {
        println("concession fare")
    }
}

Output (exit status 1)

Printed as an error (standard error)

BooleanError.kt:4:9: error: condition type mismatch: inferred type is 'Boolean?' but 'Boolean' was expected.
    if (senior) {
        ^^^^^^

Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc BooleanError.kt -include-runtime -d main.jar && java -jar main.jar

The coding conventions say to compare with == true or == false instead, which gives a plain Boolean, because null == true is false. Or decide what a skipped answer means, with ?: false:

NullableBoolean.kt: yes, no and a skipped answer Kotlin · boolean/NullableBoolean.kt
// A form asks "Senior citizen?", and a passenger can answer yes, answer no or leave it blank.
fun main() {
    val answers: List<Boolean?> = listOf(true, false, null)
    for (senior in answers) {
        val fare = if (senior == true) "concession fare" else "full fare"
        val askAgain = senior == null
        // != false looks like "yes", but it is also true for a blank answer.
        val wrongCheck = senior != false
        println("senior = $senior: $fare, ask again: $askAgain, as Boolean: ${senior ?: false}, != false: $wrongCheck")
    }
}

Output

senior = true: concession fare, ask again: false, as Boolean: true, != false: true
senior = false: full fare, ask again: false, as Boolean: false, != false: false
senior = null: full fare, ask again: true, as Boolean: false, != false: true

Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc NullableBoolean.kt -include-runtime -d main.jar && java -jar main.jar

senior == true gives the concession only to a passenger who said yes, a skipped answer pays the full fare, and senior == null finds the passengers to ask again. Avoid senior != false for “said yes”: the last column shows that it is true for a skipped answer too.

Checkpoint

Check: A nullable Boolean in a condition

  1. One question senior is a Boolean?, and null means the passenger skipped the question. Which condition gives the concession only to passengers who answered yes?

    Choose one answer.

    Show the answer: One question

    Answer: senior == true

    A Boolean? has three values. == true picks out the yes answers, == false the no answers, and ?: false decides that a skipped answer counts as no.

Practise: a display name for every user

This program checks a function that picks the name to show for a user: the nickname, else the first name, else “Guest”. As given, it uses !!, and two of its five checks fail:

DisplayName.kt: five checks, two of them fail Kotlin · practice/DisplayName.kt
// Practice: fix displayName so that all five checks pass, without using !!.
// It must return the nickname, else the first name, else "Guest".
class User(val nickname: String?, val firstName: String?)

fun displayName(user: User?): String = user!!.nickname ?: user.firstName!!

fun describe(user: User?): String = if (user == null) "null" else "User(${user.nickname}, ${user.firstName})"

fun main() {
    val checks = listOf(
        User("Ash", "Asha") to "Ash",
        User(null, "Ravi") to "Ravi",
        User("Mee", null) to "Mee",
        User(null, null) to "Guest",
        null to "Guest",
    )
    var passed = 0
    for ((user, want) in checks) {
        val got = try {
            displayName(user)
        } catch (e: NullPointerException) {
            "NullPointerException"
        }
        val ok = got == want
        if (ok) passed++
        println("${if (ok) "ok  " else "FAIL"} displayName(${describe(user)}) = $got, want $want")
    }
    println("$passed of ${checks.size} checks pass")
    if (passed < checks.size) kotlin.system.exitProcess(1)
}

Output (exit status 1)

ok   displayName(User(Ash, Asha)) = Ash, want Ash
ok   displayName(User(null, Ravi)) = Ravi, want Ravi
ok   displayName(User(Mee, null)) = Mee, want Mee
FAIL displayName(User(null, null)) = NullPointerException, want Guest
FAIL displayName(null) = NullPointerException, want Guest
3 of 5 checks pass

Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc DisplayName.kt -include-runtime -d main.jar && java -jar main.jar

Copy it, rewrite displayName without !!, and run it with kotlinc DisplayName.kt -include-runtime -d name.jar && java -jar name.jar until all five checks pass. First hint: one expression is enough, with a safe call on user for each name and the Elvis operator between them.

Then work through the questions below on safe calls, the Elvis operator and the rest of this lesson. They begin where your answers so far have been weakest, and each one explains its choices:

Practice that adapts to you

Practise this lesson

10 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 fun greet(name: String) is called as greet(bookings[pnr]?.passenger). What happens, and what is a good fix?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: It does not compile; pass bookings[pnr]?.passenger ?: "guest", or call greet only when the passenger is there

    A function with a String parameter accepts only a String. A nullable argument needs a fallback, a check before the call, or a nullable parameter if greet really can handle null.

  2. Question 2 of 10 berthNumber(pnr) of Berth.kt returns null. What can its caller conclude?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: Only that some link was missing; the PNR may be unknown, waitlisted or without a berth

    A safe-call chain answers "is there a value at the end?", not "where did it stop?". When the reason matters, use ?: return at each step, as berthLabel does.

  3. Question 3 of 10 What does kind ?: "no berth" give when kind is "lower"?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: "lower"

    x ?: y gives x unless x is null, and only then works out y.

  4. Question 4 of 10 In berthLabel, after val booking = bookings[pnr] ?: return "$pnr: no such booking", what is the type of booking?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: Booking

    return and throw never give a value, so on the path where the code goes on, the left side was not null. The compiler gives the name the non-null type.

  5. Question 5 of 10 Inside nickname?.let { … }, where nickname is a String?, what is it?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: The value of nickname, as a String

    ?.let passes the value to the block as it, with the non-null type, and skips the block when the value is null.

  6. Question 6 of 10 What does val greeting = nickname?.let { "Hello, $it!" } give when nickname is null?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: null, so println printed greeting: null

    x?.let { … } is the block's result when x is there and null when it is not, so greeting has the type String?.

  7. Question 7 of 10 What did the exception from the !! in Bang.kt say?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: java.lang.NullPointerException and nothing else, with line 13 in the trace

    !! throws a NullPointerException without a message. requireNotNull, or ?: throw with a message, says what was missing.

  8. Question 8 of 10 A screen shows the berth for a PNR the user typed. Its code is bookings[pnr]!!.coach. Which rewrite fits best?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: val booking = bookings[pnr] ?: return "no such booking"

    Ask whether a missing value is a normal case. Input that users type is often wrong, so answer it with ?: return or a fallback; keep requireNotNull for values that can only be missing through a bug.

  9. Question 9 of 10 senior is a Boolean?. What does the compiler say about if (senior) { … }?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: It refuses it with a condition type mismatch, Boolean? where Boolean was expected

    The condition of an if must be a Boolean. A Boolean? becomes one with == true, == false or ?: false, each of which says what null means.

  10. Question 10 of 10 A survey counts a skipped answer as no. Which expression turns its Boolean? answer into a Boolean that does that?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: answer ?: false

    x ?: false decides that a missing answer means false. Write the default you want explicitly, so a reader sees the decision.

Interview questions

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher · Warm-up

    What is the difference between a?.b and a!!.b in Kotlin, and which should you reach for first?

    Both read b from a value a whose type is nullable. a?.b is a safe call: when a is null it gives null instead of reading b, so the result has a nullable type and the program goes on. a!!.b uses the not-null assertion: when a is null it throws a NullPointerException without a message, and otherwise it treats a as non-null. Reach for the safe call first, usually with the Elvis operator after it to say what a missing value means, as in a?.b ?: fallback, or ?: return to stop the function. Keep !! for a value that can only be null through a bug, and even there requireNotNull(a) { "…" } is better, because its exception says what was missing.

  2. Fresher to mid

    A crash report from a booking app shows a NullPointerException with no message, on the line val berth = bookings[pnr]!!.coach!!.berth!!.number. How do you find out which value was null, and how would you rewrite the line so that the next failure explains itself?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A function findCustomer(id) sometimes finds nothing. Should it return null, a default customer or throw? Decide for two callers: a checkout API that charges the customer, and a nightly report that counts orders per city.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • String never holds null and String? may; the compiler goes by the declared type, so a String? cannot go where a String is expected, and x.length on a String? is refused.
  • A safe call a?.b gives null when a is null and skips the rest of a chain, so bookings[pnr]?.coach?.berth?.number is an Int?.
  • The Elvis operator x ?: y works out y only when x is null; ?: return and ?: throw end a function at the first missing value and leave the name on the left with a non-null type.
  • if (x != null) and x?.let { … } run code only when a value is there, and a ?: after let also replaces a null that the block returns.
  • x!! throws a NullPointerException without a message when x is null, while requireNotNull(x) { "…" } names what was missing.
  • A Boolean? in a condition needs == true, == false or ?: false.

Terms in this lesson

Elvis operator
The ?: operator: x ?: y gives x when x is not null and evaluates y only when x is null. The right side can be a fallback value, return or throw.
not-null assertion
The !! operator: x!! gives x as a non-null type, and throws a NullPointerException without a message when x is null.
nullable type
A type written with ? after it, such as String?, whose values are those of the type plus null. The compiler refuses a plain call on it until the code says what happens when the value is null.
safe call
The ?. operator: a?.b reads b only when a is not null and gives null otherwise, so a chain of safe calls stops at its first null instead of throwing.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.