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
- Kotlin evaluates
expressionto produce a value of the nullable typeT?. - The compiler-inserted check compares that value against
null. - If the value is
null, Kotlin throws aNullPointerExceptionright at that point in the code, and none of the surrounding statement executes further. - If the value is not
null, the expression’s type is treated as the non-nullTfor 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. - 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)orcheckNotNull(x)over!!when you want a hard failure with a clear, custom error message instead of a bareNullPointerException. - 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 localvalinstead 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?): Intthat returns the parsed integer age frominput, or0ifinputisnullor not a valid number, without using!!anywhere. - Take the chained-assertion example from Mistake 2 and rewrite
printCityso that instead of a default string, it throws a customIllegalStateExceptionwith the message “city is required” when the city is missing, usingrequireNotNullinstead of!!. - Write a small program with a nullable
var favoriteColor: String?that is checked for null and then read inside a lambda passed toThread. First confirm that reading it directly fails to compile due to the smart-cast restriction onvar, then fix it using a localvalcopy, and print the color’s length.
Summary
!!converts a nullable typeT?into the non-null typeTby asserting, at that exact line, that the value is not null.- If the assertion is wrong, Kotlin throws a
NullPointerExceptionimmediately 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 avarare all signs that safer alternatives, like?.,?:,requireNotNull, or a localvalcopy, should be used instead.
