Null Safety

This section documents Kotlin 2.4.x on the JVM, as published at kotlinlang.org, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against kotlinlang.org before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Null safety is arguably Kotlin’s best-known feature: nullability is part of the type, checked by the compiler at compile time, rather than discovered as a NullPointerException at run time.

Nullable Types

Every type is non-nullable by default; appending ? makes it nullable, and the compiler then requires a null check before most operations on it:

var name: String = "Ada"
// name = null                  // compile error: String is non-nullable

var maybeName: String? = "Ada"
maybeName = null                 // fine -- String? explicitly allows null

// println(maybeName.length)    // compile error: maybeName might be null

Safe Call (?.)

?. calls a member only if the receiver is non-null, and evaluates to null otherwise — avoiding an explicit if (x != null) guard for a single call, and chaining cleanly across several nullable steps:

val length: Int? = maybeName?.length              // null if maybeName is null, else its length

val city: String? = user?.address?.city            // chains through several nullable links safely;
                                                      // short-circuits to null at the first null link

Elvis Operator (?:)

?: supplies a default when the left side is null — Kotlin’s concise stand-in for a ternary x != null ? x : default:

val length: Int = maybeName?.length ?: 0            // 0 if maybeName is null

fun requireUser(id: Int): User =
    findUser(id) ?: throw NoSuchElementException("no user $id")   // "?:" combined with "throw"

Because throw is itself an expression of type Nothing (see Basic Types and Variables), the second example type-checks: the Elvis operator’s result type is User regardless, since the right side either produces a User or never returns at all.

Not-Null Assertion (!!)

!! forces a nullable expression to non-null, throwing NullPointerException immediately if it actually is null. It exists as an escape hatch, but its use is a deliberate statement that null there would be a bug, not a normal condition to handle — reach for ?./?: first in ordinary code:

val length = maybeName!!.length     // throws NPE right here if maybeName is null

Safe Cast (as?)

as? attempts a cast and evaluates to null on failure, instead of throwing ClassCastException the way a plain as cast would:

val value: Any = "hello"
val asInt: Int? = value as? Int         // null -- value is not an Int, no exception thrown
val asString: String? = value as? String // "hello"

Platform Types from Java Interop

A type coming from Java code (which has no compile-time nullability information — see Kotlin and the JVM) is represented as a platform type, notated String! in error messages — Kotlin relaxes its usual compile-time null checks for it and trusts the caller to know whether it can actually be null (informed, ideally, by a @Nullable/@NonNull annotation on the Java side, which Kotlin does respect when present):

// java.util.Map<K, V>.get(key) is a platform type: Kotlin can't prove it's non-null
val value = javaMap.get("key")     // typed as "V!" -- treat it as nullable to stay safe

Treating an unannotated platform type as non-null when it can genuinely be null is one of the few ways a NullPointerException can still surface in Kotlin code — almost always at a Java interop boundary.

Smart Casts

Once the compiler has proven a value is non-null (or of a narrower type) along a particular code path, it automatically treats it that way for the rest of that path — no explicit cast needed:

fun printLength(text: String?) {
    if (text != null) {
        println(text.length)     // smart-cast to non-null String inside this branch -- no "!!" needed
    }
}

fun describe(value: Any) {
    if (value is String) {
        println(value.uppercase())   // smart-cast to String -- see Control Flow's "is" branches
    }
}

A smart cast requires the compiler to be certain the value cannot change between the check and the use — it does not apply to a mutable var property that another thread (or another part of the same class) could reassign in between, in which case a local val copy is the usual fix.

flowchart LR N(["nullable value: String?"]) --> A{"?. safe call chain"} A -->|non-null at\nevery link| B["result: T?"] A -->|null encountered| C["short-circuits to null"] B --> D{"?: Elvis operator"} C --> D D -->|value present| E(["usable non-null value"]) D -->|still null| F["fallback / default / throw"] N -.->|"instead: !!"| G{"forced non-null assertion"} G -->|actually non-null| E G -->|actually null| H(["NullPointerException"]) style H fill:#e05252,stroke:#8a1f1f,color:#fff style E fill:#3ddc84,stroke:#1b8a4c,color:#0b3d24

See Also