skip to content

At the Java interop boundary, when do you use requireNotNull versus checkNotNull, and how do they differ from the !! operator?

level: middleimportance: must knowfreq 70%

answer

  1. require -> IllegalArgumentException -> bad input
  2. check -> IllegalStateException -> bad state/invariant
  3. Both return the value AND smart-cast
  4. !! -> NullPointerException, no message, no intent
  5. Lazy message lambda, inline functions

basics

~10 s

requireNotNull checks arguments and throws IllegalArgumentException if null. checkNotNull validates internal state and throws IllegalStateException. Both return the non-null value. !! just throws a generic NullPointerException with no message.

solid answer

~40 s

All three turn a possibly-null platform value into a non-null one, but they signal *why* it failed. `requireNotNull(x) { "..." }` is for **preconditions on inputs** — it throws `IllegalArgumentException`, meaning "the caller passed bad data". `checkNotNull(x) { "..." }` is for **invariants / internal state** — it throws `IllegalStateException`, meaning "the object is in an unexpected state". Both are inline, take a lazy message lambda, and **smart-cast** the value to non-null afterward (and return it, so you can write `val v = requireNotNull(x)`). `!!` is the blunt tool: it throws `NullPointerException` with no context and communicates no intent. At a Java boundary you usually know whether a null means "bad argument" or "broken invariant", so prefer the expressive stdlib functions; reserve `!!` for throwaway code or where you have just proven non-nullity.

code

kotlin · 3 lines
kotlin
val id = requireNotNull(dto.id) { "id is required" }        // IllegalArgumentException
val session = checkNotNull(currentSession) { "no session" }  // IllegalStateException
val token = header() ?: error("missing auth header")          // error() -> IllegalStateException

go deeper

for a junior

Knows all three assert non-null and that !! can crash.

for a middle

Picks require vs check by intent and exception type and uses the returned value.

for a senior

Designs boundary guards so failures map to the right exception class for callers and observability.

for a principal

Standardizes error taxonomy across modules so 'bad input' vs 'broken invariant' is consistent and machine-distinguishable.

## Three ways to assert non-null All three convert a platform/nullable value into a non-null value, failing if it is null — but they differ in **exception type, message, and intent**. ### `requireNotNull(value) { message }` - Throws **`IllegalArgumentException`** when `value` is null. - Semantics: a **precondition on inputs** — "the caller gave me something invalid." - Inline; the message lambda is only built on failure (cheap). - **Returns** the non-null value and smart-casts it. ### `checkNotNull(value) { message }` - Throws **`IllegalStateException`** when `value` is null. - Semantics: an **invariant / state check** — "this object got into an impossible state." - Same inline + smart-cast + return behavior. ### `!!` (not-null assertion operator) - Throws a bare **`NullPointerException`** (actually `KotlinNullPointerException`/`NullPointerException`) with no custom message. - Communicates no intent and gives the reader no "why". ```kotlin fun handle(rawUser: JavaUser /* fields are platform types */) { // input contract: caller must supply an id val id = requireNotNull(rawUser.id) { "user id must not be null" } // internal invariant: by now the cache must be loaded val cache = checkNotNull(loadedCache) { "cache not initialized" } val name = rawUser.name!! // discouraged: no context if it blows up } ``` ## Why type matters at the boundary Framing a failure as `IllegalArgumentException` vs `IllegalStateException` lets callers, logs, and error handlers distinguish **"bad input"** from **"our bug"**. A generic `NullPointerException` from `!!` erases that signal. ## Companion overloads The stdlib also has `require(condition) { ... }` and `check(condition) { ... }` for boolean conditions; the `...NotNull` variants are the null-specific specializations that additionally **return** the value. There is also `error("msg")` which always throws `IllegalStateException` (handy in an Elvis fallback). ## Smart cast After `requireNotNull(x)`/`checkNotNull(x)`, the compiler knows `x` is non-null for the rest of the scope, so you can keep using `x` directly — not only the returned value.

  • Do requireNotNull/checkNotNull smart-cast the variable, or only return a value?
    Both. They return the non-null value and, because they are contract-backed stdlib functions, the original variable is smart-cast to non-null afterward.
  • What does error("msg") throw and where is it handy?
    It throws IllegalStateException and never returns (Nothing), so it fits perfectly as the right side of an Elvis: `x ?: error("...")`.

saying these in an interview costs you the question

  • Saying requireNotNull and checkNotNull throw the same exception
  • Using !! when a meaningful precondition/invariant is intended
  • Claiming the message string is always constructed even on success
  • Not knowing they return the value / smart-cast
  • Mixing up which one means 'bad argument' vs 'bad state'

context