skip to content

How do the Kotlin contracts behind require/check/requireNotNull enable smart-casting, and how would you choose the right precondition function when designing an API?

level: seniorimportance: should knowfreq 30%

answer

  1. Contract: returns() implies (cond) -> smart-cast
  2. requireNotNull/checkNotNull also return non-null T
  3. error/TODO return Nothing -> unreachable
  4. require=caller, check=state, error=impossible, assert=optional
  5. Smart-cast needs a stable val / provable var

basics

~20 s

These functions tell the compiler, via contracts, what is guaranteed if they return without throwing. That lets the compiler narrow types (e.g. to non-null or to a subtype). When designing APIs, pick require for bad inputs, check for bad state, and error for unreachable code.

solid answer

~40 s

Each precondition function declares a Kotlin contract. require(condition) uses returns() implies condition, so after a require the compiler knows condition held — if condition is an is/null check, the variable smart-casts. requireNotNull/checkNotNull use returns() implies (value != null), narrowing to non-null. error/TODO return Nothing, so contracts plus the bottom type mark following code unreachable. Design-wise: require for caller-supplied arguments (fail fast at the boundary, IllegalArgumentException), check for invariants about the receiver/program state (IllegalStateException), error for impossible branches, and assert only for optional internal checks. Put guards at the top of the function, give precise lazy messages, and keep the exception type meaningful so callers and crash reporters can distinguish a client error from a state bug.

code

kotlin · 12 lines
kotlin
sealed interface Shape
data class Circle(val r: Double) : Shape
data class Square(val s: Double) : Shape

fun area(shape: Shape, scale: Double): Double {
    require(scale > 0) { "scale must be > 0, was $scale" } // caller error
    return when (shape) {
        is Circle -> Math.PI * shape.r * shape.r
        is Square -> shape.s * shape.s
        // no else needed; if it were, use: else -> error("unknown $shape")
    } * scale
}

go deeper

for a junior

Recognizes that after these guards you can use the value without extra null/type checks and knows the basic require/check split.

for a middle

Explains return()-implies contracts at a high level and applies the require/check/error choice correctly in API code.

for a senior

Details smart-cast stability rules, Nothing-based unreachability, and maps exception types to client-vs-server error semantics.

for a principal

Establishes precondition and error-taxonomy conventions across services, ties exception types to HTTP/observability layers, and weighs experimental-contract usage.

## Contracts in one paragraph A **contract** is metadata a function declares via the experimental `kotlin.contracts` DSL telling the compiler what is true when it returns. The relevant clause here is `returns() implies (booleanExpression)`: "if this function returns normally (does not throw), then `booleanExpression` is true." The compiler then propagates that fact into smart-casts. ## How each guard uses contracts - `require(condition)` declares `returns() implies condition`. So: ```kotlin fun render(x: Any) { require(x is String) println(x.length) // x smart-cast to String } ``` - `requireNotNull(value)` / `checkNotNull(value)` declare `returns() implies (value != null)` AND return the value as a non-null `T`. Both ways narrow the type: ```kotlin val s = requireNotNull(maybe) // s: String (return value) // or, discarding the return value, `maybe` itself is smart-cast to non-null ``` - `check(condition)` mirrors `require` with `returns() implies condition`. - `error(message)` / `TODO()` return `Nothing`, marking the rest of a branch unreachable (so a `when`/`?:` stays exhaustive/typed). Smart-cast requires a **stable** value: a `val`, or a `var` the compiler can prove is unchanged (not a custom getter, not a delegated property, not a captured-and-mutated var). ## Choosing the right function (API design) ```kotlin fun connect(host: String?, retries: Int) { requireNotNull(host) // bad argument -> IllegalArgumentException require(retries >= 0) { "retries >= 0, was $retries" } check(state == State.IDLE) { "already connecting" } // bad state -> IllegalStateException } ``` Guidelines: 1. **require** for everything coming from the **caller** — fail fast at the API boundary; the `IllegalArgumentException` blames the caller. 2. **check** for invariants about the **receiver / program state** — `IllegalStateException` says "the object isn't ready," independent of arguments. 3. **error** for genuinely unreachable code (default `else`, sealed exhaustiveness) — leverages `Nothing`. 4. **assert** only for optional, internal, possibly-disabled sanity checks. 5. Place guards **first**, keep messages precise and lazy, and never disable-able guards (assert) for real contracts. ## Why the type matters beyond syntax The chosen exception type is part of the contract surface: a client-error (`IllegalArgumentException`) might map to an HTTP 400, while a state bug (`IllegalStateException`) is a 500. Consistency here pays off in error handling and observability.

  • Why does smart-cast after requireNotNull fail for a property with a custom getter?
    A custom getter could return a different value on each access, so the compiler cannot guarantee stability and refuses to narrow the type.
  • When should a precondition violation be a check (IllegalStateException) rather than a require (IllegalArgumentException)?
    When the failure is about the receiver's/program's state being wrong for the operation rather than a specific argument the caller passed.

Contracts are the function leaving a signed note for the compiler: 'if you got here, this fact is guaranteed' — and the compiler trusts it for type narrowing.

saying these in an interview costs you the question

  • Not knowing contracts are what enable the smart-cast
  • Using require for state and check for arguments
  • Claiming smart-cast works on any var including custom getters
  • Treating exception-type choice as arbitrary
  • Using error/TODO without understanding the Nothing return type

context