skip to content

Explain how a Nothing-returning function or `throw` enables expressions like `val x = a ?: error("...")` and why the compiler then treats following code as unreachable.

level: middleimportance: should knowfreq 55%

answer

  1. Elvis type = common supertype of branches
  2. Nothing branch collapses to the other type
  3. throw / error / return are Nothing expressions
  4. Unconditional Nothing call → unreachable warning
  5. Conditional exit → smart-cast on surviving path

basics

~20 s

Because a throw or a Nothing function can stand in for any type, you can put it after Elvis (?:) to get a non-null value or bail out. The compiler knows that path can't continue, so code after it is dead.

solid answer

~40 s

The Elvis operator `a ?: b` has the type that is the common supertype of `a`'s non-null type and `b`'s type. When `b` is `throw ...` or a `Nothing`-returning call like `error(...)`/`fail(...)`, its type is `Nothing`, the bottom type, which is a subtype of everything — so the result type is simply `a`'s non-null type. That is why `val x: String = a ?: error("...")` smart-casts `x` to non-null `String`. The compiler's control-flow analysis treats a `Nothing` result as a point of no return: statements after an unconditional Nothing call are flagged **unreachable**, and after a conditional one (e.g., inside an `if`) the negative branch's facts (like non-nullness) propagate as smart casts. This is the same machinery used by `requireNotNull`, `checkNotNull`, and `return`/`break`/`continue` in expression position.

code

kotlin · 5 lines
kotlin
fun firstUpper(words: List<String>?): Char {
    val list = words ?: return ' '          // return : Nothing
    val head = list.firstOrNull() ?: error("empty")  // error : Nothing
    return head.first().uppercaseChar()
}

go deeper

for a junior

Can use a ?: return/a ?: throw but may not explain the typing.

for a middle

Explains the Elvis result type and that throw/error/return are Nothing expressions.

for a senior

Connects Nothing to control-flow analysis: unreachable code plus smart-cast on the surviving path.

for a principal

Reasons about least-common-supertype rules and how bottom-type substitution keeps inference sound across these idioms.

## The type of an Elvis expression `a ?: b` evaluates to `a` when `a` is non-null, otherwise to `b`. Its static type is the **least common supertype** of `a`'s non-null type and `b`'s type. - If `b` has type `Nothing` (the bottom type, subtype of everything), the common supertype collapses to `a`'s non-null type. ```kotlin fun parse(s: String?): Int { val text: String = s ?: throw IllegalArgumentException("null input") // ^ type Nothing, so `text` is non-null String return text.length } ``` `throw` is an **expression** of type `Nothing`, and `error(msg): Nothing`, `TODO(): Nothing`, or your own `fun fail(): Nothing` work the same way. ## Why following code becomes unreachable Kotlin runs a **control-flow analysis** pass. A call returning `Nothing` is a guaranteed exit, so: ```kotlin fun demo(x: Int) { fail("stop") // : Nothing println("never runs") // warning: unreachable code } ``` The compiler emits an *unreachable code* warning for anything after an unconditional Nothing call. ## Smart casts via the negative branch When the Nothing exit is conditional, the compiler learns facts on the surviving path: ```kotlin fun useName(user: User) { val name = user.name ?: return // null path exits the function // here `name` is smart-cast to non-null println(name.uppercase()) } ``` Here `return` is also of type `Nothing`. The same applies to `break`, `continue`, and `requireNotNull(x)` (which returns the non-null value and throws otherwise). ## Where the standard library leans on this - `requireNotNull(x)` / `checkNotNull(x)` → return non-null, throw on null. - `require(cond) { msg }` / `check(cond) { msg }` → fail path is Nothing-typed. - `error(msg)` and `TODO()` → both `: Nothing`. ## Key terms - **Least common supertype**: the most specific type assignable from both branches. - **Control-flow analysis**: the compiler pass that tracks reachability and nullness. - **Smart cast**: automatic narrowing of a value's type after a proven check.

  • What is the static type of `a ?: throw E()` when `a: String?`?
    String (non-null), because the throw branch is Nothing, the subtype of everything, so the result is a's non-null type.
  • Does `requireNotNull(x)` rely on Nothing?
    Its throwing path is Nothing-typed; the function returns the non-null value on success, enabling smart casts and `val y = requireNotNull(x)`.

saying these in an interview costs you the question

  • Claiming the Elvis result is nullable even with a Nothing right side
  • Not knowing throw/return are expressions of type Nothing
  • Thinking unreachable-code warnings come from a linter, not the compiler
  • Failing to connect Nothing to smart casts
  • Believing `?: error(...)` could still yield null

context