Question

Kotlin Nullable Types: Idiomatic Ways to Handle, Convert, and Assert Non-Null Values

kotlinnullablenon-nullable

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.

  • Xyz means the value cannot be null
  • Xyz? means the value may be null

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 an Xyz
  • Xyz? = 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 inside
  • x?.foo() = use the item only if the box is not empty
  • x ?: fallback = if the box is empty, use a backup item
  • x!! = 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."

Take Quiz

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

  1. map is created with two entries:

    • "a" -> 65
    • "b" -> 66
  2. 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
  3. ?: 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
  4. After the Elvis operator, number has type Int, not Int?

  5. number.toLong() is allowed because number is now definitely non-null

  6. 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" }
Take Quiz

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

ApproachResult TypeIf value is nullBest used when
x?.foo()Nullable resultReturns nullNull is acceptable
x ?: fallbackNon-null if fallback is non-nullUses fallbackYou want a default
if (x != null)Non-null inside blockSkips blockYou need multiple operations
x?.let { ... }Depends on blockSkips blockYou want scoped non-null work
x!!Non-nullThrows NullPointerExceptionYou are absolutely sure and want brevity
requireNotNull(x)Non-nullThrows IllegalArgumentExceptionInput validation
checkNotNull(x)Non-nullThrows IllegalStateExceptionInternal state validation
map[key]NullableReturns nullMissing key is normal
map.getValue(key)Non-null for non-null value typeThrows if missingKey must exist

!! vs requireNotNull

Feature!!requireNotNull
Short syntaxYesNo
Custom messageNoYes
Intent is clearLessMore
Failure typeNullPointerExceptionIllegalArgumentException

map[key] vs getValue(key)

ExpressionReturn typeMissing key behavior
map[key]V?Returns null
map.get(key)V?Returns null
map.getValue(key)VThrows exception
Take Quiz

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 if or let for conditional work
  • Use requireNotNull / checkNotNull for 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.

Take Quiz

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

  1. Create a Map<String, Int> containing at least three role names and numeric access levels.
  2. Read one optional role that may or may not exist in the map.
  3. Print a default access level when the role is missing.
  4. Read one required role and fail clearly if it does not exist.
  5. Convert a non-null access level to Long and print it.
Take Quiz

Keep learning

Related questions

View more