skip to content

When is `x?.let { }` strictly better than `if (x != null) { }`, and what is one subtle gotcha when `x?.let` is paired with `?:`?

level: middleimportance: should knowfreq 65%

answer

  1. `?.let` shines for mutable `var`/custom getters (no smart cast)
  2. `?.let` captures value once into `it`
  3. `?: fallback` triggers on lambda result being null, not on receiver
  4. Two null sources get conflated in `let { } ?: x`
  5. Pure local `val`? plain `if` is fine

basics

~20 s

?.let is better when x is a mutable property the compiler won't smart-cast. The gotcha: if you write x?.let { ... } ?: fallback, the fallback also runs when the lambda itself returns null — not only when x is null.

solid answer

~50 s

For a **local `val`**, `if (x != null)` smart-casts and is just as good. The clear win for `?.let` is a **mutable `var`** or a **property with a custom getter** — the compiler can't smart-cast those because the value might change between check and use, so `if (x != null) x.foo()` won't compile, but `x?.let { it.foo() }` captures the value once. The subtle gotcha is the common `x?.let { ... } ?: fallback`: the Elvis `?:` fires whenever the **left side is null**, and the left side is the *lambda's* result. So if the lambda's last expression evaluates to `null`, `fallback` runs too — even though `x` was non-null. If you only want the fallback for a null `x`, restructure (e.g. `if (x == null) fallback else { ... }`) or make the lambda return a guaranteed non-null sentinel.

go deeper

for a junior

Can use ?.let correctly but may not articulate the smart-cast reason or the Elvis gotcha.

for a middle

Names the mutable-property/custom-getter smart-cast limitation and recognizes the let { } ?: x double-null trap.

for a senior

Discusses single-evaluation semantics, when if is clearer, and chooses also vs let by return-value intent.

for a principal

Sets team guidance on the Elvis-on-lambda anti-pattern and reasons about readability/maintainability across a codebase.

## The smart-cast limitation Kotlin's **smart cast** lets the compiler treat a nullable as non-null after a check like `if (x != null)`. But smart casts are only sound when the compiler can prove the value can't change between the check and the use. It **refuses to smart-cast** for: - a **mutable `var`** (local or property), - a **property with a custom getter** (calling it twice could yield different results), - a property defined in **another module**. ```kotlin class Box { var item: String? = null } fun render(b: Box) { // if (b.item != null) println(b.item.length) // WON'T COMPILE: b.item is mutable b.item?.let { println(it.length) } // OK: captured as a stable parameter `it` } ``` `?.let` evaluates `b.item` **once** and binds it to the immutable lambda parameter `it`, sidestepping the limitation. This is the canonical reason to prefer `?.let`. ## The `?: fallback` gotcha A very common pattern is: ```kotlin val label = user?.let { formatName(it) } ?: "anonymous" ``` The Elvis operator `?:` returns its right side when the **left side is null**. Here the left side is **the result of the lambda**, not `user`. So: - `user == null` -> lambda skipped, left side is null -> `"anonymous"`. (intended) - `user != null` but `formatName(it)` returns `null` -> left side is null -> `"anonymous"` runs **anyway**. (often unintended!) If you meant "only fall back when the user is absent," this conflates two different null reasons. Fixes: ```kotlin // Option A: keep the result nullable, handle separately if (user == null) "anonymous" else formatName(user) // Option B: ensure the lambda never returns null val label = user?.let { formatName(it) ?: "unnamed" } ?: "anonymous" ``` ## Side-effect ordering Because `?.let { }` is an expression that returns the lambda result, putting **side effects** in it that you expect to always run can be surprising when the receiver is null. For pure side effects you sometimes want `also` (returns the receiver) instead. ## Keywords/APIs named smart cast, `var`/`val`, custom getter, `?.`, `let`, `it`, Elvis `?:`, `also`.

  • Why can't the compiler smart-cast a property with a custom getter?
    Two reads of the getter may return different values, so a non-null first read doesn't guarantee the second is non-null; the cast would be unsound.
  • How do you make `user?.let { f(it) } ?: g()` run `g()` only when `user` is null?
    Don't chain Elvis onto the lambda; use `if (user == null) g() else f(user)`, or guarantee the lambda returns non-null.

saying these in an interview costs you the question

  • Claiming `if (x != null)` always smart-casts, even for `var` properties
  • Saying `let { } ?: fallback` only fires the fallback when the receiver is null
  • Not knowing `?.let` evaluates the receiver only once
  • Reaching for `?.let` on a local `val` where a plain `if` is clearer

context