skip to content

Compare contract-based narrowing (requireNotNull, isNullOrEmpty) with alternatives like the Elvis operator, !!, and plain if-checks for null/type narrowing. When does the contract approach earn its keep, and what does it cost?

level: seniorimportance: nice to knowfreq 22%

answer

  1. Inline if/is/Elvis: no contract needed
  2. !! narrows but throws bare NPE
  3. Contracts = delegate check, keep smart cast + good exception
  4. Best for reusable named preconditions
  5. Cost: opt-in + unverified trust + stability

basics

~20 s

Contracts let helper functions carry the narrowing so callers stay clean and get a real smart cast plus a clear exception. Elvis and if-checks narrow inline without any helper; !! narrows but throws a vague NPE. Contracts cost an experimental opt-in and unverified trust.

solid answer

~40 s

Inline checks (`if (x != null)`, `x ?: return`, `x is T`) already give smart casts and need no contracts because the compiler sees them directly. The Elvis operator handles a value-or-default/early-exit and narrows on the non-null path. `!!` narrows but throws an undescriptive NullPointerException and bypasses validation intent. Contracts earn their keep when you want to **delegate** the check to a named, reusable function (requireNotNull, check, isNullOrEmpty) and still keep the caller's smart cast and a meaningful exception (IllegalArgumentException/IllegalStateException) — turning a precondition into a self-documenting one-liner. Costs: the `ExperimentalContracts` opt-in, the unverified trust model (a wrong contract crashes at the call site), and call-site stability requirements still apply. Rule of thumb: inline narrowing for one-off local checks; contract-bearing helpers for repeated, named preconditions where the descriptive exception and reuse pay off.

go deeper

for a junior

Knows requireNotNull avoids !! and gives a smart cast.

for a middle

Picks the right tool (Elvis vs require vs inline if) for a given narrowing need.

for a senior

Articulates the reuse + descriptive-exception payoff of contracts versus the opt-in and trust costs.

for a principal

Frames it as an API-ergonomics and maintainability tradeoff and when a custom contract-bearing predicate is worth introducing across a codebase.

## The alternatives, side by side ### Plain if / is checks (no contract needed) ```kotlin if (x is String) { x.length } // smart cast, compiler sees it directly if (x != null) { x.length } ``` The compiler analyzes these inline; contracts add nothing here. ### Elvis operator `?:` ```kotlin val s = x ?: return // x non-null after this line val n = x?.length ?: 0 // value-or-default ``` Great for early-exit or defaulting. It narrows on the surviving path but does not produce a *named, reusable* precondition. ### Not-null assertion `!!` ```kotlin val len = x!!.length ``` Narrows, but throws a bare `NullPointerException` with no message about *why*, and signals "I'm overriding the type system" — usually a smell for preconditions. ### Contract-bearing stdlib helpers ```kotlin requireNotNull(x) { "x must be set" } // IllegalArgumentException with message x.length // smart-cast, no !! needed check(state is Ready) // IllegalStateException if (!s.isNullOrEmpty()) s.length // contract narrows s ``` Here the **contract** lets the helper hand the smart cast back to the caller while giving a descriptive, intent-revealing exception type and lazy message lambda. ## When contracts earn their keep - **Reusable, named preconditions** used across many call sites (`requireNotNull`, `check`). - **Descriptive failure**: `IllegalArgumentException`/`IllegalStateException` with a message beats a bare NPE from `!!`. - **Readability**: `requireNotNull(cfg)` reads as intent; `cfg ?: error("...")` is fine too but contracts also cover `is`-narrowing predicates like `isNullOrEmpty`. - **Custom domain predicates**: a `returns(true) implies (x is Money)` predicate centralizes a check and still narrows callers. ## What they cost - **Experimental opt-in** (`@OptIn(ExperimentalContracts::class)`) for *your own* contracts (stdlib ones are already opted in for you). - **Unverified trust**: a wrong contract crashes at the use site (see soundness). - **Stability rules** still gate the smart cast (val/unmodified local). - Slight cognitive overhead — a reader must know the helper carries a contract. ## Decision guide | Situation | Prefer | |---|---| | One-off local null/type check | `if` / `is` / Elvis (no contract) | | Default a nullable | Elvis `?:` | | Override types you're sure about, one place | `!!` (sparingly) | | Named, reusable precondition + smart cast + good exception | `require`/`check`/`requireNotNull` (contracts) | | Reusable boolean predicate that should narrow callers | custom `returns(true/false) implies` |

  • Why prefer requireNotNull(x) over x!! for a precondition?
    requireNotNull throws IllegalArgumentException with a custom lazy message describing the broken precondition and reads as intent; x!! throws an opaque NullPointerException and signals fighting the type system.
  • Do you need contracts for if (x is String) { ... }?
    No. The compiler sees the inline is-check and smart-casts directly. Contracts are only needed to carry narrowing across a function-call boundary.

saying these in an interview costs you the question

  • Claiming inline if-checks need contracts to smart-cast
  • Recommending !! for routine preconditions
  • Ignoring that custom contracts need an opt-in while stdlib ones don't
  • Forgetting the descriptive-exception benefit of require/check

context