Question
Kotlin Nullable Types: Idiomatic Ways to Handle, Convert, and Assert Non-Null Values
Question
In Kotlin, how should you idiomatically work with a nullable type such as Xyz? when you need to reference it or treat it as a non-nullable Xyz?
For example, this code produces an error:
val something: Xyz? = createPossiblyNullXyz()
something.foo() // Error: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Xyz?
However, if a null check is added first, the call is allowed:
val something: Xyz? = createPossiblyNullXyz()
if (something != null) {
something.foo()
}
Why does this work?
Also, how can a nullable value be treated as non-null without always writing an if check, when you know for certain it is not actually null?
For example, when retrieving a value from a map:
val map = mapOf("a" to 65, "b" to 66, "c" to 67)
val something = map.get("a")
something.toLong() // Error: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Int?
Here, get() returns Int? because the key might not exist. What is the idiomatic way to turn that result into a non-null value when you know the key is present?
Short Answer
By the end of this page, you will understand how Kotlin nullable types work, why a prior null check allows direct access, and the idiomatic tools for handling nullable values: safe calls, if checks, let, Elvis ?:, requireNotNull, checkNotNull, and the non-null assertion operator !!. You will also learn the best way to access map values when a missing key is or is not acceptable.
Concept
Kotlin distinguishes between nullable and non-nullable types.
Xyzmeans the value cannot benullXyz?means the value may benull
This is one of Kotlin's key safety features. It helps prevent the classic NullPointerException problem by making nullability part of the type system.
If a variable has type Xyz?, Kotlin will not let you call methods on it directly, because the value might be null at runtime.
val something: Xyz? = createPossiblyNullXyz()
something.foo() // Not allowed
Kotlin requires you to do one of the following:
- prove the value is not null
- safely handle the null case
- explicitly assert that it is not null
That is why this works:
if (something != null) {
something.foo()
}
Inside the if block, Kotlin uses smart casting. Because you checked something != null, the compiler can treat something as Xyz instead of Xyz? in that scope.
Why this matters
In real programs, values are often uncertain:
- API responses may omit fields
- map lookups may fail
- database columns may be nullable
- user input may be missing
Kotlin gives you clear, explicit ways to deal with that uncertainty instead of letting null-related bugs appear later.
Idiomatic ways to handle nullable values
1. Safe call: ?.
Call a method only if the value is not null.
something?.foo()
If something is null, the whole expression becomes null and no method is called.
2. Elvis operator: ?:
Provide a fallback value when null is possible.
val length = name?.length ?: 0
3. Null check with smart cast
Use if when you need a block of non-null operations.
if (something != null) {
something.foo()
something.bar()
}
4. let
Run code only when a value is non-null.
something?.let {
it.foo()
}
5. Non-null assertion: !!
Force Kotlin to treat the value as non-null.
something!!.foo()
If something is actually null, this throws a NullPointerException.
This is valid Kotlin, but it should be used carefully.
6. requireNotNull / checkNotNull
Assert non-null with a clearer intention and error message.
val value = requireNotNull(something) { "something must not be null" }
value.foo()
These are often more expressive than !!.
Map lookups
A map lookup returns a nullable type because the key may be missing.
val map = mapOf("a" to 65, "b" to 66)
val value: Int? = map["a"]
Even if you know the key exists, Kotlin cannot assume that from the type alone.
Idiomatic options include:
val value = map.getValue("a")
getValue() is often the best choice when the key is expected to exist. It returns a non-null value for maps with non-null value types and throws if the key is missing.
Other options:
val value = map["a"]!!
val value = requireNotNull(map["a"])
val value = map["a"] ?: error("Missing key: a")
These differ mainly in readability and the kind of failure they produce.
Mental Model
Think of a nullable value like a box that may or may not contain an item.
Xyz= a box that definitely contains anXyzXyz?= a box that might be empty
Kotlin does not let you use the item unless you first deal with the possibility that the box is empty.
There are several ways to handle it:
if (x != null)= open the box and verify something is insidex?.foo()= use the item only if the box is not emptyx ?: fallback= if the box is empty, use a backup itemx!!= say, "I guarantee this box is not empty"
That last option is like skipping the safety check. If you are wrong, the program crashes.
For map lookups, imagine asking a dictionary for a word. The dictionary may or may not contain it, so Kotlin gives you a nullable result unless you use an API that says, "This key must exist."
Syntax and Examples
Core syntax
val a: String? = null // nullable
val b: String = "hello" // non-nullable
1. Safe call ?.
val name: String? = getName()
val length = name?.length
If name is null, length becomes null.
2. Elvis operator ?:
val name: String? = getName()
val length = name?.length ?: 0
If name is null, length becomes 0.
3. Smart cast after null check
val name: String? = getName()
if (name != null) {
println(name.length)
}
Inside the if, name is treated as String.
4. let for non-null execution
val name: String? = getName()
name?.let {
println(it.uppercase())
}
The block runs only when name is not null.
5. Non-null assertion !!
val name: String? = getName()
println(name!!.length)
This forces a non-null type, but crashes if name is null.
6. requireNotNull
val name: String? = getName()
val nonNullName = requireNotNull(name) { "Name was unexpectedly null" }
println(nonNullName.length)
This is often clearer than !! because the intent is explicit.
7. Map access
val map = mapOf("a" to 65, "b" to 66, "c" to 67)
val x1 = map["a"] // Int?
val x2 = map.getValue("a") // Int
val x3 = map["a"] ?: 0 // Int
Example: converting nullable lookup to non-null
val map = mapOf("a" to 65, "b" to 66, "c" to 67)
val something = map.getValue("a")
println(something.toLong())
This is idiomatic when the key is expected to exist.
Step by Step Execution
Consider this example:
val map = mapOf("a" to 65, "b" to 66)
val number = map["a"] ?: error("Key not found")
val result = number.toLong()
println(result)
Step by step
-
mapis created with two entries:"a" -> 65"b" -> 66
-
map["a"]performs a lookup.- The return type is
Int? - Even though
"a"exists here, the map API still returns a nullable type because some keys might be missing
- The return type is
-
?: error("Key not found")handles the null case.- If the result is not null, that value is used
- If the result is null, the program throws an exception immediately
-
After the Elvis operator,
numberhas typeInt, notInt? -
number.toLong()is allowed becausenumberis now definitely non-null -
The output is:
65
Why the compiler allows this
Because after ?: error(...), there is no remaining null path. Either:
- you get a real
Int, or - execution stops with an exception
That lets Kotlin safely treat number as non-null.
Real World Use Cases
API and JSON parsing
Many API fields are optional.
val email = user.email ?: "no-email@example.com"
Form input validation
User input may be missing or blank.
val username = requireNotNull(inputUsername) { "Username is required" }
Database access
A query may return no result.
val customer = repository.findById(id)
customer?.let {
sendEmail(it)
}
Configuration values
Use a fallback if an environment setting is absent.
val port = System.getenv("APP_PORT")?.toInt() ?: 8080
Map and cache lookups
Caches and maps often return nullable results.
val user = cache[userId] ?: loadUser(userId)
Defensive programming
Stop early when a required value is unexpectedly null.
val token = checkNotNull(session.token) { "Session token missing" }
Real Codebase Usage
In real Kotlin codebases, developers usually choose a nullable-handling style based on intent.
Common patterns
Guard clauses
Fail early when a required value is missing.
val user = requireNotNull(currentUser) { "User must be logged in" }
This avoids deeply nested code.
Early return
Return from a function if a nullable value is absent.
fun printLength(text: String?) {
val value = text ?: return
println(value.length)
}
Fallback values
Use sensible defaults.
val timeout = config.timeout ?: 30
Transforming nullable values
Use safe calls and scope functions.
val upper = name?.trim()?.uppercase()
Required map entries
Use getValue() when the key must exist.
val statusCode = codes.getValue("ok")
Validation and business rules
Use requireNotNull for function arguments and external input.
fun process(email: String?) {
val safeEmail = requireNotNull(email) { "email is required" }
println(safeEmail.lowercase())
}
Internal state checks
Use checkNotNull when a value should already be initialized.
val connection = checkNotNull(activeConnection) { "Connection not initialized" }
When !! appears in real projects
It does appear, but usually in limited cases:
- tests
- migration code
- framework integration where a contract is guaranteed elsewhere
- very local code where null is impossible and adding more syntax would reduce clarity
Even then, teams often prefer clearer alternatives like requireNotNull or checkNotNull.
Common Mistakes
1. Using !! too often
Broken style:
val name: String? = getName()
println(name!!.length)
This works only if name is truly not null. If you are wrong, it crashes.
Better:
val name = requireNotNull(getName()) { "Name is required" }
println(name.length)
2. Forgetting that map lookup returns nullable
Broken code:
val map = mapOf("a" to 1)
val value = map["a"]
println(value.toString())
value is Int?, not Int.
Better:
val value = map.getValue("a")
println(value.toString())
or
val value = map["a"] ?: 0
println(value.toString())
3. Using safe calls when you actually need a non-null result
val length = name?.length
This gives you Int?, not Int.
If you need a real Int, add a fallback:
val length = name?.length ?: 0
4. Assuming the compiler always smart-casts
Smart casts work only when Kotlin can prove the value does not change unexpectedly.
Example that may not smart-cast reliably in more complex situations:
var name: String? = getName()
if (name != null) {
println(name.length)
}
This simple example is fine, but if the variable is mutable, captured, or comes from a property with custom logic, smart-casting may not be possible.
A common fix is to assign to a local val.
5. Hiding missing-data bugs with bad defaults
val price = productPrice ?: 0
Sometimes 0 is a useful fallback. Sometimes it hides a bug.
Ask whether null means:
- missing but acceptable
- missing and should fail
- missing and should trigger a default
Choose the handling style accordingly.
6. Confusing cast with null handling
A type cast does not magically prove non-nullability.
Broken idea:
val value: String? = getValue()
val forced = value as String
This can throw if null is present.
Prefer explicit null handling or requireNotNull.
Comparisons
| Approach | Result Type | If value is null | Best used when |
|---|---|---|---|
x?.foo() | Nullable result | Returns null | Null is acceptable |
x ?: fallback | Non-null if fallback is non-null | Uses fallback | You want a default |
if (x != null) | Non-null inside block | Skips block | You need multiple operations |
x?.let { ... } | Depends on block | Skips block | You want scoped non-null work |
x!! | Non-null | Throws NullPointerException | You are absolutely sure and want brevity |
requireNotNull(x) | Non-null | Throws IllegalArgumentException | Input validation |
checkNotNull(x) | Non-null | Throws IllegalStateException | Internal state validation |
map[key] | Nullable | Returns null | Missing key is normal |
map.getValue(key) | Non-null for non-null value type | Throws if missing | Key must exist |
!! vs requireNotNull
| Feature | !! | requireNotNull |
|---|---|---|
| Short syntax | Yes | No |
| Custom message | No | Yes |
| Intent is clear | Less | More |
| Failure type | NullPointerException | IllegalArgumentException |
map[key] vs getValue(key)
| Expression | Return type | Missing key behavior |
|---|---|---|
map[key] | V? | Returns null |
map.get(key) | V? | Returns null |
map.getValue(key) | V | Throws exception |
Cheat Sheet
Nullable basics
val a: String = "hello" // non-null
val b: String? = null // nullable
Access patterns
x?.foo() // safe call
x ?: fallback // Elvis operator
x!!.foo() // force non-null, may crash
Smart cast
if (x != null) {
x.foo()
}
Run code only if not null
x?.let {
println(it)
}
Fail fast with message
val value = requireNotNull(x) { "x must not be null" }
val state = checkNotNull(y) { "y was not initialized" }
Map lookups
val value1 = map[key] // V?
val value2 = map.getValue(key) // V
val value3 = map[key] ?: default // V
Good default choices
- Use
?.when null is okay - Use
?:when you have a sensible fallback - Use
iforletfor conditional work - Use
requireNotNull/checkNotNullfor assertions - Use
!!sparingly - Use
getValue()when a map key must exist
Important rule
A nullable type does not become non-null just because you believe it should be. You must prove it, handle it, or assert it in code.
FAQ
Why does Kotlin allow access after if (x != null)?
Kotlin uses smart casting. After that check, the compiler knows x cannot be null inside the block, so it treats it as non-null.
What is the most idiomatic replacement for !!?
Often requireNotNull, checkNotNull, or ?: with a meaningful fallback or error is clearer and safer.
Should I use map[key]!! or map.getValue(key)?
If the key is required to exist, map.getValue(key) is usually more expressive. It communicates intent better than !!.
What is the difference between requireNotNull and checkNotNull?
requireNotNull is for validating inputs or arguments. checkNotNull is for validating internal program state.
Does ?. convert a nullable value to non-null?
No. It safely accesses the value and produces another nullable result.
Can I cast Xyz? to Xyz to remove nullability?
You can write a cast, but it does not avoid the runtime risk. Explicit null handling is usually better and clearer.
Why does Map.get() return a nullable value even when my key exists?
Because the API is designed for the general case where the key may be absent. The compiler only trusts the type contract, not your assumption.
When is !! acceptable?
Usually in small, controlled situations where null is impossible by design, such as some tests or framework boundaries. Even then, use it carefully.
Mini Project
Description
Build a small Kotlin program that reads user roles from a map and prints a formatted access level. This project demonstrates how to work with nullable map lookups, provide defaults, and fail fast when a required key is missing.
Goal
Create a program that safely handles nullable values from map access using ?:, getValue(), and requireNotNull.
Requirements
- Create a
Map<String, Int>containing at least three role names and numeric access levels. - Read one optional role that may or may not exist in the map.
- Print a default access level when the role is missing.
- Read one required role and fail clearly if it does not exist.
- Convert a non-null access level to
Longand print it.
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Add a Maven Repository URL in Gradle Kotlin DSL (build.gradle.kts)
Learn how to add Maven Central and a custom Maven repository URL in Gradle Kotlin DSL using build.gradle.kts syntax.
Allow HTTP and HTTPS in Android 9 Pie with Network Security Configuration
Learn how Android 9 Pie handles cleartext HTTP traffic and how to allow HTTP and HTTPS safely using network security config.