skip to content

Compare a smart cast after `if (x != null)` with the `!!` not-null assertion and with `?.`/`let`. When is each appropriate?

level: seniorimportance: should knowfreq 50%

answer

  1. Smart cast = proven, no failure point
  2. `!!` = assert non-null, NPE if wrong
  3. `?.` + `?:` = safe call with fallback
  4. `?.let {}` = block on non-null, fixes unstable receivers
  5. var / custom getter / open -> no smart cast, capture to val

basics

~20 s

A smart cast lets the compiler prove the value is non-null after a check, with no risk. !! forces non-null and crashes if you're wrong. ?./let handle the null case gracefully. Prefer smart cast or safe calls; use !! rarely.

solid answer

~50 s

After `if (x != null)`, the compiler smart-casts `x` to non-null with a *proof* — there is no runtime failure point. `x!!` is the **not-null assertion operator**: it throws `NullPointerException` if `x` is null, so it asserts what you couldn't prove and shifts safety onto you. `x?.foo()` (safe call) evaluates to `null` instead of calling when `x` is null, often paired with the Elvis operator `?: default`. `x?.let { ... }` runs a block only when non-null, with `it` smart-cast inside. Prefer smart casts and safe calls; reach for `!!` only when invariants the compiler can't see guarantee non-null (and even then a clear failure message via `?: error(...)` is usually better). A frequent reason developers fall back to `!!` is an *unstable* receiver (a `var`, a property with a custom getter, or a mutable property accessed across threads) where the compiler refuses to smart-cast; capturing into a local `val` or using `?.let` restores safety.

code

kotlin · 8 lines
kotlin
class Session(var token: String?)

fun decode(s: Session): Int {
    // s.token is a var property: smart cast is refused
    val t = s.token ?: return -1   // safe: capture stable val
    return t.length                // t: String, smart-cast-stable
    // vs s.token!!.length -> NPE if it became null after the check
}

go deeper

for a junior

Knows smart cast is safe and !! can crash, and prefers ?./smart cast.

for a middle

Explains ?., ?:, and ?.let and when to use each over !!.

for a senior

Identifies why unstable receivers (var, custom getter, open) block smart casts and chooses local-val capture or ?.let over !!.

for a principal

Treats !! as an invariant-assertion smell, advocates ?: error(...) for diagnosable failures, and reasons about thread-visibility of mutable nullable properties.

## The four tools ### 1. Smart cast — `if (x != null) { x.foo() }` Compiler-proven narrowing; **no runtime failure point** at the use site. Best when you have a stable `val` and a plain null/type check. ### 2. Not-null assertion — `x!!` `!!` converts `T?` to `T` and **throws `NullPointerException`** if the value is null. It's an assertion of a fact the compiler can't verify. Use sparingly; an unexplained `!!` that fires gives a bare NPE with no context. ```kotlin val len = name!!.length // NPE if name is null ``` ### 3. Safe call — `x?.foo()` Returns `null` (rather than calling) when the receiver is null. Combine with the **Elvis operator** for a fallback: ```kotlin val len = name?.length ?: 0 ``` ### 4. Scope function — `x?.let { ... }` Runs the lambda only when non-null; inside, the value (as `it`) is non-null. Useful for *unstable* properties because the captured lambda parameter is effectively a stable local: ```kotlin user.address?.let { addr -> println(addr.city) // addr: Address, no smart-cast-stability concern } ``` ## Why not always smart cast? Smart casts require a **stable** reference. Common cases where the compiler refuses (and people reach for `!!` or `?.let`): - a `var` (could change between check and use) - a property with a **custom getter** (could return a different value each read) - an **open** property (a subclass could override it) - a `var` property of another module / accessed across threads ```kotlin class Box(var item: String?) fun useBad(b: Box) { if (b.item != null) { // b.item is a var property -> NOT smart-cast // println(b.item.length) // compile error val it = b.item ?: return // capture into local val println(it.length) } } ``` Capturing into a local `val` (or `?.let`) is the idiomatic fix — safer than `!!`. ## Guidance - Prefer **smart cast** when the receiver is stable. - Prefer **`?.` + `?:`** for inline null handling with a default. - Prefer **`?.let`** when you need a block and the receiver is unstable. - Use **`!!`** only for genuine invariants, ideally replaced by `?: error("why")` for a meaningful message. Keywords/operators: `!!`, `?.`, `?:`, `let`, `it`, stable `val`, custom getter, `open`.

  • Why does the compiler refuse to smart-cast a property with a custom getter?
    A custom getter is a function that may return a different value on each access, so the value proven non-null at the check might be null at the use site; the compiler can't assume stability.
  • What's a better alternative to a bare `!!`?
    `?: error("meaningful message")` or `?: throw IllegalStateException(...)` — it documents the invariant, fails with context, and (via the Nothing result) still narrows the value.

Smart cast is a verified ID badge; !! is you swearing you're allowed in and getting tackled if you lied.

saying these in an interview costs you the question

  • Defaults to `!!` everywhere instead of smart casts / safe calls
  • Doesn't know why a `var` or custom-getter property isn't smart-cast
  • Claims `!!` and a smart cast are equally safe
  • Suggests `!!` instead of capturing the property into a local `val`

context