skip to content

Compare the Elvis operator with alternatives like ?.let, requireNotNull/checkNotNull, and !! for handling nullable values. When would you choose each?

level: seniorimportance: should knowfreq 50%

answer

  1. ?: = default or exit
  2. requireNotNull/checkNotNull = assert + return value
  3. checkNotNull throws IllegalState; requireNotNull throws IllegalArgument
  4. !! = bare NPE, last resort
  5. ?.let = transform when present, composes with ?:

basics

~20 s

Use ?: to supply a default or to exit early on null. Use requireNotNull/checkNotNull to assert a value must exist with a clear message. Avoid !!, which throws an unhelpful NPE. Use ?.let to run code only when non-null.

solid answer

~40 s

`?:` is for **producing a value or exiting**: `x ?: default` (substitute) or `x ?: return/throw` (guard). `requireNotNull(x)`/`checkNotNull(x)` **return** the non-null value or throw `IllegalArgumentException`/`IllegalStateException` with a lazily-built message — ideal for precondition/invariant checks where you want the value, not a default. `!!` throws a bare `NullPointerException` and should be reserved for cases the compiler can't prove but you can — overuse is a code smell. `?.let { }` runs a block **only when non-null**, returning the block's result (or null), which differs from Elvis: Elvis chooses between values, `let` transforms the present value. They compose: `x?.let { transform(it) } ?: fallback`. Choose Elvis for defaults/guards, requireNotNull for asserting-with-message, let for conditional transformation, and `!!` rarely.

code

kotlin · 6 lines
kotlin
fun resolve(raw: String?, cfg: Config?): String {
    // assert with message, get value
    val c = requireNotNull(cfg) { "config required" }
    // transform when present, else default
    return raw?.let { it.trim() }?.ifEmpty { null } ?: c.defaultName
}

go deeper

for a junior

Knows ?: gives a default and that !! can crash; basic awareness of alternatives.

for a middle

Picks ?: vs requireNotNull vs !! correctly for default vs assert vs proven-non-null.

for a senior

Distinguishes require vs check exception semantics, composes ?.let ?:, and explains the let-returns-null pitfall.

for a principal

Establishes team policy banning casual !!, standardizing assertion helpers, and consistent error types across modules.

## The toolbox for nullable values ### Elvis `?:` — substitute or exit ```kotlin val port = configPort ?: 8080 // default val user = repo.find(id) ?: return null // guard / early exit val u2 = repo.find(id) ?: throw NotFound(id) ``` Use when you want **a concrete fallback value** or a **fail-fast early exit** inline. ### requireNotNull / checkNotNull — assert and get the value Both are stdlib functions that **return the non-null value** or throw: - `requireNotNull(x) { "msg" }` → throws `IllegalArgumentException` (for **argument** validation). - `checkNotNull(x) { "msg" }` → throws `IllegalStateException` (for **state/invariant** validation). The message lambda is **only evaluated on failure** (lazy). Prefer these over `x ?: throw ...` when the thrown type and semantics match, because they read as intent ("this must not be null") and standardize messages. ```kotlin fun start(cfg: Config?) { val c = requireNotNull(cfg) { "config must be provided" } // c: Config (non-null) } ``` ### Not-null assertion !! — last resort `x!!` returns `x` if non-null else throws `NullPointerException` with little context. It defeats null safety and should appear only when you have proof the compiler lacks (e.g., after external validation it can't track). Teams often lint against `!!`. Prefer `?:`, `requireNotNull`, or smart-casting guards instead. ### ?.let — conditional transformation `?.let { block }` invokes `block` with the non-null value as `it` **only when the receiver is non-null**, returning the block's result, else `null`. This is **not** the same as Elvis: `let` is about doing/transforming when present; Elvis is about choosing a value. They combine: ```kotlin val label = rawName?.let { it.trim().uppercase() } ?: "UNKNOWN" ``` ## Decision guide | Need | Use | |------|-----| | Provide a default value | `x ?: default` | | Early-exit on null | `x ?: return` / `x ?: throw` | | Assert non-null, keep value, clear message | `requireNotNull` / `checkNotNull` | | Transform only when present | `x?.let { ... }` (often with `?:`) | | You can prove non-null, compiler can't | `!!` (sparingly) | ## A subtle pitfall: let returning null If the `let` block can itself return `null` (or the value is a valid result that equals a falsy-looking default), `x?.let { ... } ?: fallback` may apply the fallback when the block returned `null`, not only when `x` was null. Be intentional about which null you're defaulting.

  • Why prefer requireNotNull over `x ?: throw IllegalArgumentException(...)`?
    It expresses intent succinctly, returns the value, and standardizes the exception type and lazy message; the Elvis-throw form is fine when you need a custom exception type.
  • How can `x?.let { } ?: d` surprise you?
    If the let block itself yields null, the Elvis fallback `d` is applied even though `x` was non-null.
  • What does !! throw and why avoid it?
    A bare NullPointerException with poor context; it bypasses null-safety and hides where the real invariant should be checked.

saying these in an interview costs you the question

  • Reaching for !! as the default tool
  • Thinking ?.let and ?: are interchangeable
  • Confusing requireNotNull (IllegalArgument) with checkNotNull (IllegalState)
  • Believing the requireNotNull message is always evaluated
  • Not seeing the let-returns-null edge case

context