RizTech Academy logo
RizTech Academy
Null SafetyLesson 3 of 425 min

The Elvis operator, let, and smart casts

The safe call gives you null when something is absent. But often you do not want null — you want a fallback, a sensible default, or to run a block only when the value is present. This lesson covers the two tools for that: the Elvis operator ?: and the let function, plus how they combine into some of the most idiomatic lines in Kotlin.

The Elvis operator ?:

The Elvis operator provides a default when the left side is null:

val name: String? = null
val display = name ?: "Guest"
println(display)              // Guest

val realName: String? = "Kavita"
println(realName ?: "Guest")  // Kavita

Read a ?: b as "a, or if that is null, b". It is the clean answer to "use this value, but fall back to something if it is missing". (The name is a joke — turned sideways, ?: looks like Elvis Presley's hair.)

It pairs perfectly with the safe call to turn a nullable chain into a guaranteed value:

val city: String? = customer?.address?.city
val label = customer?.address?.city ?: "Unknown city"
println(label)                // "Unknown city" if any link was null

customer?.address?.city ?: "Unknown city" is a complete, idiomatic line: walk the nullable chain, and if anything along it is null, use the default. The result is a plain String — non-null, because the Elvis guaranteed it. This is how you get from String? to String: give a fallback.

Elvis with early return or throw

The right side of ?: can be any expression — including return or throw, which is enormously useful for the "handle absence at the top" pattern:

fun greet(name: String?) {
    val realName = name ?: return       // if null, leave the function now
    println("Namaste, $realName")       // realName is String from here on
}

fun requireCity(customer: Customer?): String {
    return customer?.address?.city
        ?: throw IllegalArgumentException("Customer has no city")
}

val realName = name ?: return reads as "get the name, or give up". After that line, realName is a guaranteed non-null String, so the rest of the function is clean. This is the Elvis version of the early-return smart-cast pattern from the last lesson, and it is one of the most common shapes in real Kotlin: use the value, or bail out — in one line.

The let function

let runs a block with the value as its argument, and returns the block's result. Its most valuable use is with ?., to run code only when a value is non-null:

val name: String? = "Kavita"
name?.let {
    println("Name is $it")        // runs only if name is non-null
    println("It has ${it.length} letters")
}

val missing: String? = null
missing?.let {
    println("This never runs")     // skipped entirely — missing is null
}

Inside the let block, it is the non-null value (you can also name it: name?.let { n -> ... }). The ?.let { } combination means "if this is present, do this with it" — a very common need, and much cleaner than a nested if (x != null) { ... } when you have a short block to run.

let also shines for transforming a nullable into something else with a fallback:

val input: String? = "42"
val number = input?.let { it.toIntOrNull() } ?: 0
println(number)               // 42, or 0 if input was null or not a number

Combining them — the idiomatic lines

Elvis and let together produce the lines that mark someone who knows Kotlin. Read these slowly; each is a pattern you will reuse constantly:

// "use the value if present, else a default"
val display = username ?: "Guest"

// "transform if present, else a default"
val length = username?.length ?: 0

// "do something if present" (no else needed)
username?.let { saveToDatabase(it) }

// "get it or give up"
val user = findUser(id) ?: return

// "get it or fail loudly"
val user = findUser(id) ?: error("No user with id $id")

Notice what these have in common: null is handled inline, at the point of use, in one readable expression — no separate if block, no temporary variable, no crash. That density-with-clarity is the payoff of Kotlin's null handling, and it is why nullable types feel like a help rather than a tax once you have the operators.

A word on overusing let

let is lovely, and beginners who discover it start putting it everywhere. Resist. A plain if (x != null) with a smart cast is often clearer than x?.let { }, especially for a longer block or when you also need an else branch. Reach for ?.let { } when you have a short action to run only on the non-null case; reach for if/Elvis otherwise. The idiomatic-Kotlin lesson in the best-practices module returns to this — the scope functions (let, run, also, apply, with) are powerful and easy to overuse into unreadable code.

Check your work

What a ?: b means. a, or if a is null, b — a default for a nullable value.

How to turn a String? into a String. Give a fallback with ?: (or handle null another way).

What name ?: return does. Leaves the function if name is null; otherwise binds a guaranteed non-null value.

What can go on the right of ?:. Any expression, including return, throw, and error(...).

What ?.let { } does. Runs the block only if the value is non-null, with it bound to the non-null value.

How to transform a nullable with a fallback. x?.let { transform(it) } ?: default.

The common thread of the idiomatic lines. Null handled inline at the point of use, in one readable expression.

When to prefer if/smart-cast over ?.let. For a longer block, or when you need an else — a plain if (x != null) is clearer.

Practice

  1. Use ?: to give a nullable name a default of "Guest". Test with a value and with null.
  2. Write val n = input?.length ?: 0 and test with a string and with null.
  3. Write a function that does val name = param ?: return and prints only when a name was given.
  4. Write one that does ?: throw IllegalArgumentException(...) and observe the exception when the value is null.
  5. Use name?.let { } to print two lines about the name, and confirm the block is skipped when the name is null.
  6. Chain it: input?.let { it.toIntOrNull() } ?: -1, and test with "42", "abc", and null.
  7. Take a nested if (x != null) { println(x) } and rewrite it as x?.let { println(it) }. Then decide, for a five-line block, which you would actually prefer.

Official documentation

Next: !!, and why it is almost always the wrong answer.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship