skip to content

What does error(message) do, and why do require/check use a lazy message lambda instead of a plain String parameter?

level: middleimportance: should knowfreq 45%

answer

  1. error(msg) = throw IllegalStateException, returns Nothing
  2. Nothing -> usable on right of ?: and as when branch
  3. Message is () -> Any lambda, runs only on failure
  4. inline -> lambda inlined, no allocation
  5. Happy path pays zero for the message

basics

~10 s

error(msg) always throws an IllegalStateException with that message and never returns. The lazy lambda means the message is only built when the check actually fails, so it costs nothing when everything is fine.

solid answer

~30 s

error(message: Any) throws IllegalStateException(message.toString()) unconditionally. Its return type is Nothing, so the compiler knows code after it is unreachable and it can be used in expression position, e.g. val x = map[key] ?: error("missing"). For the lazy message: require/check/requireNotNull/checkNotNull all have an overload whose second parameter is lazyMessage: () -> Any. The lambda is invoked only on the failure path, so an expensive string (interpolation, joinToString, etc.) is never built on the happy path. Because these functions are inline, the lambda is inlined too, so there is no extra lambda allocation or call overhead. This combines safety with zero steady-state cost.

code

kotlin · 7 lines
kotlin
fun pick(id: Int, table: Map<Int, String>): String =
    table[id] ?: error("no entry for id=$id")  // Nothing lets this sit after ?:

fun validate(items: List<Int>) {
    require(items.isNotEmpty()) { "items empty; got ${items.joinToString()}" }
    // joinToString only runs if the list is actually empty
}

go deeper

for a junior

Knows error(msg) throws an IllegalStateException and that the message lambda saves work when the check passes.

for a middle

Explains the Nothing return type enabling Elvis/when usage and that the lazy lambda only runs on failure.

for a senior

Connects inline to zero lambda allocation and contrasts with a plain-String API that always evaluates the message.

for a principal

Reasons about Nothing in the type system, exhaustiveness, and cost-free defensive checks as a codebase-wide guideline.

## error(message) `error(message: Any): Nothing` always throws `IllegalStateException(message.toString())`. Two things make it useful: - It is for **impossible / invalid state** reached anyway (e.g. an exhaustive `when` that should never hit a branch). - Its return type is `Nothing` — the bottom type that means "never returns normally." That lets the compiler treat following code as unreachable and lets `error(...)` sit on the right of `?:` or as a `when` branch value: ```kotlin val color = map[key] ?: error("no color for $key") val label = when (state) { State.A -> "a" State.B -> "b" else -> error("unexpected $state") } ``` ## Why a lazy message lambda The signatures are like: ```kotlin public inline fun require(value: Boolean, lazyMessage: () -> Any) ``` The message is a **lambda** (`() -> Any`), not a `String`. The body of `require` only calls `lazyMessage()` on the failure branch: ```kotlin // conceptual if (!value) { val msg = lazyMessage(); throw IllegalArgumentException(msg.toString()) } ``` Consequences: - **No work on the happy path.** A costly message — string interpolation, `list.joinToString()`, serializing an object — is built only when the check actually fails. - **No allocation, thanks to `inline`.** These functions are `inline`, so the lambda is inlined at the call site; there is no `Function0` object created and no virtual call. You get the deferral for free. Contrast a plain-String API, `require(value, "expensive ${build()}")`, which would evaluate the string on **every** call even when the precondition passes. ## error vs throw `error("x")` is just sugar for `throw IllegalStateException("x")`, but shorter and with the `Nothing` return type making expression use ergonomic.

  • Why can error(...) be used as the right-hand side of the Elvis operator ?:
    Because it returns Nothing, which is a subtype of every type, so it fits wherever a value is expected while actually throwing.
  • If require were not inline, would the lazy message still help?
    It would still defer evaluation to the failure path, but you would pay a lambda allocation on every call; inline removes that cost.

The lazy message is like only printing the error report when the machine actually breaks, not on every successful run.

saying these in an interview costs you the question

  • Saying error returns Unit instead of Nothing
  • Claiming error throws IllegalArgumentException
  • Thinking the message string is always built regardless of outcome
  • Not connecting Nothing to expression/Elvis usage
  • Believing the lambda causes an allocation despite inline

context