skip to content

How does TODO()'s Nothing return type interact with control-flow analysis, such as when used in a branch of a when or as a default elvis fallback?

level: middleimportance: should knowfreq 35%

answer

  1. Nothing = bottom type, subtype of all
  2. Dead path after TODO(): no 'must return' error
  3. when-expression branch: Nothing fits result type
  4. x ?: TODO() narrows x to non-null
  5. Same as throw/error/return for flow analysis

basics

~10 s

Because TODO() never returns normally, the compiler knows code after it can't run. So you can drop it into a branch that needs a value, or after ?:, and the types still line up.

solid answer

~40 s

TODO() returns Nothing, which is a subtype of every type and signals 'unreachable beyond here'. In a `when` used as an expression, each branch must yield the common type; a branch like `else -> TODO()` satisfies that because Nothing fits any expected type, while also keeping the `when` exhaustive. After the elvis operator, `val x = maybe ?: TODO("missing")` makes x non-null (`String` not `String?`), because the right side of ?: has type Nothing and the result type is the union narrowed to the left's non-null type. Smart-casts also leverage Nothing: after a Nothing-returning call the compiler marks the path dead, so it won't demand a return or flag a variable as possibly uninitialized. This is the same machinery `error()`, `throw`, and `return` use.

code

kotlin · 7 lines
kotlin
fun area(shape: Shape): Double = when (shape) {
    is Circle -> Math.PI * shape.r * shape.r
    is Square -> shape.side * shape.side
    is Triangle -> TODO("need base and height") // compiles; throws if a Triangle reaches it
}

val token: String = headers["Authorization"] ?: TODO("auth not wired yet")

go deeper

for a junior

Recognizes you can put TODO() in a branch and it still compiles.

for a middle

Explains Nothing as the bottom type and how it satisfies when-branch result types and narrows ?: results to non-null.

for a senior

Connects TODO() to definite-assignment/unreachable-code analysis and equates it with throw/error/return for flow purposes.

for a principal

Advises using Nothing-returning stubs to preserve exhaustiveness during refactors and guards against shipping live TODO() with CI/lint policy.

## Why `Nothing` is special `Nothing` is the **bottom type**: it is a subtype of every other Kotlin type and has no values. An expression typed `Nothing` can never produce a value, because reaching it always diverges (throws or loops). The compiler's control-flow analysis treats a `Nothing`-typed expression as a **dead end** — anything after it on that path is unreachable. `TODO()` is declared `: Nothing`, so it inherits all of this. The same is true of `throw`, `return`, `error(...)`, and infinite loops. ## In a `when` expression When `when` is used as an **expression** (its value is assigned/returned), every branch must produce the result type, and the `when` must be **exhaustive** (cover all cases). A `Nothing`-returning branch fits both rules: ```kotlin fun describe(x: Status): String = when (x) { Status.OK -> "fine" Status.WARN -> "careful" Status.ERROR -> TODO("format error detail") // type Nothing <: String, OK } ``` The `TODO()` branch type-checks as `String` (Nothing is a subtype of String) and keeps the result type `String`. ## After the elvis operator `?:` `a ?: b` has type = the common supertype of (non-null `a`) and `b`. If `b` is `Nothing`, the result is just the non-null type of `a`: ```kotlin val name: String = config["name"] ?: TODO("name is required") // config["name"] is String?; because the right side is Nothing, // name is smart-narrowed to non-null String. ``` This is the canonical pattern with `error(...)` and works identically with `TODO()`. ## Definite assignment / 'must return a value' Because the path after `TODO()` is dead, the compiler won't require a subsequent `return`, and won't complain a `val` is possibly uninitialized if it is only assigned on the live paths: ```kotlin fun pick(flag: Boolean): Int { val v: Int if (flag) v = 1 else TODO("handle false") return v // OK: false branch is unreachable } ``` ## Practical note Replacing a `when`/elvis fallback with `TODO()` is a great way to keep a half-finished function compiling **and** keep exhaustiveness honest — when you later add the real branch the compiler still forces you to cover every case. Don't, however, ship `TODO()` on a live path: it will throw `NotImplementedError` for users.

  • Why does `val s: String = map[k] ?: TODO()` give s a non-null type?
    The right side of ?: has type Nothing, so the result type collapses to the non-null type of the left operand (String), not String?.
  • Which other expressions share this control-flow behaviour with TODO()?
    throw, return, error(...), and infinite loops — all evaluate to Nothing or diverge, so the compiler marks following code unreachable.

TODO() is a trapdoor: the compiler sees anyone stepping on it falls through, so it stops asking 'what happens after?'.

saying these in an interview costs you the question

  • Thinking the when branch needs a cast to compile
  • Claiming x ?: TODO() leaves x nullable
  • Believing the compiler still requires a return after TODO()
  • Confusing Nothing (bottom type) with Unit or Any?
  • Saying TODO() only works in functions, not expressions

context