skip to content

What does fail() return, and how does that Nothing return type let you use it in expressions?

level: seniorimportance: should knowfreq 45%

answer

  1. fail() returns Nothing
  2. Nothing = bottom type, subtype of all
  3. usable in ?:, when, if branches
  4. code after fail is unreachable
  5. smart-casts the surviving branch to non-null

basics

~20 s

fail() always makes the test fail immediately. It returns Nothing, the type with no values, so the compiler treats anything after it as unreachable and lets you use it anywhere a value is expected, like the right side of ?:.

solid answer

~40 s

`fail(message: String? = null): Nothing` unconditionally fails the test. Because its return type is `Nothing` — the bottom type that is a subtype of every type and has no instances — the compiler knows control never returns from it. That makes code after `fail()` unreachable and lets `fail()` appear wherever a value is required: as a `when`/`if` branch, the right operand of the elvis operator (`header ?: fail("missing")`), or to satisfy a non-null/typed result. Typical uses: a 'should have thrown' guard in manual try/catch, marking an impossible branch, or a loud TODO stub. For exception assertions specifically, `assertFailsWith<T> { }` is usually cleaner, but that is a sibling topic.

code

kotlin · 9 lines
kotlin
import kotlin.test.fail

val token: String = header ?: fail("missing auth header")
// token is non-null here because the fail branch can't produce a value

val r = when (state) {
    State.OK -> compute()
    else     -> fail("impossible state")
}

go deeper

for a junior

Knows fail() makes the test fail and can take a message.

for a middle

Knows fail returns Nothing and that following code is unreachable.

for a senior

Explains Nothing as the bottom type and uses fail() in expression positions like ?: and when branches.

for a principal

Reasons about Nothing in the type lattice, exhaustiveness, and when fail() vs assertFailsWith expresses intent most clearly.

## `fail()` — unconditional failure `fail(message: String? = null): Nothing` immediately fails the current test. Its return type is `Nothing`, so the compiler treats code after it as unreachable and it satisfies any expected return type. ```kotlin import kotlin.test.fail val result: Result = when (state) { State.OK -> compute() State.ERROR -> fail("computation entered an impossible state") } ``` ### Common uses - **"Should have thrown" guards** in manual try/catch: ```kotlin try { parse("garbage") fail("expected NumberFormatException") } catch (e: NumberFormatException) { assertEquals("garbage", e.message?.substringAfter(": ")) } ``` (For most cases `assertFailsWith<NumberFormatException> { ... }` is cleaner — but that is a sibling topic.) - Marking an unreachable `else`/`when` branch. - Stubbing a not-yet-implemented test so it fails loudly rather than silently passing (`fail("TODO")`). ### `Nothing` makes it composable Because `fail` returns `Nothing` (the type with no values, a subtype of every type), you can use it as the right-hand side of `?:`, a `when` branch, or anywhere a value is expected: ```kotlin val token = header ?: fail("missing auth header") ``` The variable `token` is non-null afterward because the `fail` branch can never produce a value.

  • Why does `val x: Int = cond ?: fail("...")` compile even though fail returns Nothing?
    Nothing is a subtype of Int (of every type), so it satisfies the expected type; and because it never returns, x can only be the non-fail value.
  • When would you use fail() over assertFailsWith?
    assertFailsWith is preferred for exception tests; fail() suits impossible branches, non-exception 'should not reach here' guards, or TODO stubs.

Nothing is like a trapdoor: any code path that hits it never comes back, so the compiler stops worrying about what type 'should' come out.

saying these in an interview costs you the question

  • Says fail returns Unit or Boolean
  • Doesn't know what Nothing is
  • Thinks code after fail() still runs
  • Can't explain why fail works as an elvis right-hand side
  • Uses fail+try/catch where assertFailsWith is clearly better

context