The Not-Null Assertion (!!)

Kotlin’s type system splits every type into a nullable version, like String?, and a non-null version, like String, and the compiler refuses to build code that might dereference a nullable value without checking it first. The not-null assertion operator, !!, is Kotlin’s escape hatch from that check: it tells the compiler to treat a nullable value as non-null right now, no questions asked. If you are right, the program continues normally. If you are wrong, Kotlin throws a NullPointerException on the spot, which is exactly the crash null safety was designed to prevent. Understanding !! means understanding both when it is a legitimate tool and, far more often, when it is a warning sign that proper null handling was skipped.

Overview: How the Not-Null Assertion Works

A variable of type String is guaranteed by the compiler to never hold null. A variable of type String? might hold null, and anywhere you try to call a method or read a property on it, the compiler forces you to prove it isn’t null first. Kotlin gives you several tools to do that proving: a safe call (?.), an elvis operator (?:), an explicit if (x != null) check that lets the compiler smart-cast the variable, or the not-null assertion, !!.

The !! operator converts a value of type T? into a value of type T by asserting, at that exact point in the code, that the value is not null. Under the hood, the compiler emits a runtime null check at that location, conceptually similar to calling Objects.requireNonNull, before letting execution continue. If the check passes, the expression’s static type becomes non-null and everything chained after it is treated that way with no further null checks required. If the check fails, Kotlin throws a NullPointerException immediately, with a message identifying which expression was null.

This makes !! fundamentally different from every other null-handling tool in Kotlin. Safe calls and the elvis operator let your program keep running by skipping an operation or substituting a default. The not-null assertion does the opposite: it trades a compile-time guarantee, the type system says this cannot be null, for a runtime gamble, I am asserting this cannot be null and if I am wrong the program crashes. Because of that trade-off, !! exists mainly for two legitimate situations: when you have information the compiler cannot infer, for example a value you validated moments earlier in a way the compiler cannot track, or when you are interoperating with Java code whose nullability isn’t annotated and you are certain from context that a value cannot be null. Outside those cases, reaching for !! usually means a safe call, an elvis operator, or a proper validation function would serve you better.

Syntax

The not-null assertion has a single, simple form:

expression!!
Part Meaning
expression Any expression whose static type is nullable, such as T?
!! The not-null assertion operator
Result type T (non-null), if the expression evaluates to a non-null value
Failure behavior Throws NullPointerException immediately if the expression evaluates to null

You can apply !! directly to a variable (name!!), a function call result (findUser(id)!!), or a property access (user.email!!), and you can chain further calls onto the unwrapped result, as in name!!.length.

Examples

Example 1: A safe, successful assertion

When the value genuinely isn’t null, !! simply unwraps it and execution proceeds as if the type had always been non-null.

fun main() {
    val name: String? = "Kotlin"
    val length: Int = name!!.length
    println("Length: $length")
}

Output:

Length: 6

Here name is declared as String?, so accessing .length directly wouldn’t compile. The !! asserts that name is not null, which is true, so the assertion succeeds silently and length ends up as a plain Int.

Example 2: An assertion that fails

The exact same code crashes the moment the value actually is null, because the assertion has no fallback, unlike ?:.

fun main() {
    val name: String? = null
    val length: Int = name!!.length
    println("Length: $length")
}

Output:

The program crashes before printing anything. It throws a NullPointerException at the line "val length: Int = name!!.length" because name is null at that point; execution never reaches the println call.

This is the core risk of !!: it looks identical whether the value is null or not, so the crash only shows up when bad data actually flows through that line, often much later than when the bug was introduced.

Example 3: A realistic case, and a better alternative

Consider looking up several users by id, where some ids might not exist in the backing map.

fun findUser(id: Int): String? {
    val users = mapOf(1 to "Alice", 2 to "Bob")
    return users[id]
}

fun main() {
    val idsToLookUp = listOf(1, 3)
    for (id in idsToLookUp) {
        val name = findUser(id) ?: "Unknown user"
        println("User $id: $name")
    }
}

Output:

User 1: Alice
User 3: Unknown user

Map indexing in Kotlin returns V? because the key might not be present, so findUser correctly returns String?. Instead of writing findUser(id)!!, which would crash the whole loop the moment id 3 didn’t resolve, the elvis operator supplies a sensible fallback and the program keeps running for every id. This is the pattern !! tempts you to skip, and skipping it is almost always the wrong call.

How It Works Step by Step

  1. Kotlin evaluates expression to produce a value of the nullable type T?.
  2. The compiler-inserted check compares that value against null.
  3. If the value is null, Kotlin throws a NullPointerException right at that point in the code, and none of the surrounding statement executes further.
  4. If the value is not null, the expression’s type is treated as the non-null T for the rest of the statement, so any calls or property accesses chained after the !! compile and run exactly as they would on a genuinely non-nullable value.
  5. Because the check happens at the exact call site, the resulting stack trace points precisely to the !! that failed, which is more informative than a raw platform NPE with no context, but still just as fatal if uncaught.

Common Mistakes

Mistake 1: Reaching for !! out of habit

Many developers coming from Java use !! reflexively anywhere the compiler complains about nullability, instead of asking whether the value could genuinely be null and handling that case.

fun printLength(s: String?) {
    println(s!!.length)
}

fun main() {
    printLength("Kotlin")
    printLength(null) // crashes here
}

This compiles fine and works for non-null input, but it silently reintroduces the exact NullPointerException risk that null safety exists to eliminate. A caller who legitimately has no value for s now crashes the whole program instead of getting sensible behavior.

fun printLength(s: String?) {
    val length = s?.length ?: 0
    println(length)
}

fun main() {
    printLength("Kotlin")
    printLength(null)
}

The safe call plus elvis version handles both cases without ever risking a crash, and it documents the intended fallback behavior directly in the code.

Mistake 2: Chaining multiple !! together

Assertions are easy to overuse across a chain of nullable properties, which makes failures nearly impossible to diagnose.

class Address(val city: String?)
class User(val address: Address?)

fun printCity(user: User?) {
    println(user!!.address!!.city!!.uppercase())
}

fun main() {
    val user = User(Address(null))
    printCity(user) // crashes, but which !! failed?
}

All three assertions look identical in a stack trace error, so when this line crashes, you cannot tell at a glance whether user, address, or city was the null one.

class Address(val city: String?)
class User(val address: Address?)

fun printCity(user: User?) {
    val city = user?.address?.city ?: "Unknown city"
    println(city.uppercase())
}

fun main() {
    val user = User(Address(null))
    printCity(user)
}

Output:

UNKNOWN CITY

A single chain of safe calls followed by one elvis fallback is both crash-proof and easier to read than three separate assertions.

Mistake 3: Using !! to work around a smart-cast error on a var

Smart-casting only applies when the compiler can prove a value can’t change between the null check and its use. A var captured inside a lambda, such as one passed to Thread, can in principle be reassigned before the lambda runs, so the compiler refuses to smart-cast it even after a null check.

var name: String? = "Kotlin"
if (name != null) {
    Thread {
        println(name.length) // compile error: smart cast to String impossible, name is a var
    }.start()
}

Faced with that compiler error, it’s tempting to just add !! and move on.

var name: String? = "Kotlin"
if (name != null) {
    Thread {
        println(name!!.length)
    }.start()
}

Output:

6

That compiles and usually works, but it papers over the real issue: name could theoretically be reassigned to null by other code between the check and the moment the thread runs, and the assertion would then crash unpredictably. The cleaner fix is to copy the var into a local val, which the compiler can safely smart-cast because a val can never be reassigned.

fun main() {
    var name: String? = "Kotlin"
    val safeName = name
    if (safeName != null) {
        Thread {
            println("Length: ${safeName.length}")
        }.start()
    }
}

Output:

Length: 6

No assertion is needed at all, and the code is safe even if the original name variable changes later.

Best Practices

  • Try a safe call (?.) and elvis operator (?:) before ever reaching for !!; they cover the vast majority of real null-handling needs.
  • Prefer requireNotNull(x) or checkNotNull(x) over !! when you want a hard failure with a clear, custom error message instead of a bare NullPointerException.
  • Never chain more than one !! in a single expression; each additional assertion makes the eventual crash harder to attribute to a specific value.
  • When a smart cast fails on a var, copy the value into a local val instead of silencing the compiler with !!.
  • Treat every !! in a code review as something that needs a one-line comment justifying why the value truly cannot be null at that point.
  • At Kotlin/Java interop boundaries, prefer converting an unannotated Java value into a Kotlin nullable type explicitly and handling it with ?:, rather than asserting it away with !!.

Practice Exercises

  • Write a function parseAge(input: String?): Int that returns the parsed integer age from input, or 0 if input is null or not a valid number, without using !! anywhere.
  • Take the chained-assertion example from Mistake 2 and rewrite printCity so that instead of a default string, it throws a custom IllegalStateException with the message “city is required” when the city is missing, using requireNotNull instead of !!.
  • Write a small program with a nullable var favoriteColor: String? that is checked for null and then read inside a lambda passed to Thread. First confirm that reading it directly fails to compile due to the smart-cast restriction on var, then fix it using a local val copy, and print the color’s length.

Summary

  • !! converts a nullable type T? into the non-null type T by asserting, at that exact line, that the value is not null.
  • If the assertion is wrong, Kotlin throws a NullPointerException immediately at that point in the code.
  • Unlike ?. and ?:, which let the program keep running, !! trades a compile-time null-safety guarantee for a runtime crash risk.
  • !! is appropriate only when you have information the compiler can’t infer, or at Java interop boundaries where you’re certain a value cannot be null.
  • Overusing !!, chaining multiple assertions together, or using !! to silence a smart-cast error on a var are all signs that safer alternatives, like ?., ?:, requireNotNull, or a local val copy, should be used instead.