One of the most praised features of Kotlin is its built-in null safety system. If you've worked with Java or other languages, you've probably experienced the frustration of a NullPointerException crashing your app at runtime. Kotlin eliminates this entire category of bugs by making nullability a core part of the type system. This guide explains everything clearly with real examples.


What Is Null?

null means "no value" or "nothing here." It's not zero, not an empty string — it's the complete absence of a value.

var name: String = "John"   // has a value
var name: String = null     // null — no value at all

The problem with null is when you try to use a variable that is null as if it has a value — that causes a crash.

// Java — this compiles fine but crashes at runtime
String name = null;
int length = name.length();  // NullPointerException — app crashes!

Kotlin prevents this by making you declare upfront whether a variable can be null or not, and then forcing you to handle the null case before using the value.


Nullable vs Non-Nullable Types

In Kotlin, every type is non-nullable by default. That means a regular String can never hold null.

var name: String = "John"
name = null   // ❌ compile error — String cannot be null

To allow null, you add a ? after the type name — this creates a nullable type:

var name: String? = "John"  // can hold a String
name = null                  // ✅ allowed — String? can be null

This simple ? is the foundation of Kotlin's null safety. Every type has a nullable counterpart:

var a: Int = 5        // cannot be null
var b: Int? = null    // can be null

var c: Boolean = true    // cannot be null
var d: Boolean? = null   // can be null

var e: Double = 3.14     // cannot be null
var f: Double? = null    // can be null

The Problem — You Can't Use Nullable Variables Directly

Once you have a nullable variable, Kotlin won't let you use it as if it were definitely not null. The compiler forces you to handle the null case first.

var nickname: String? = "Johnny"

val length = nickname.length  // ❌ compile error
// Kotlin says: nickname could be null — you must handle that case

Kotlin gives you four clean ways to handle this.


1. Safe Call Operator — ?.

The safe call operator ?. calls the method or accesses the property only if the value is not null. If it is null, the whole expression returns null instead of crashing.

var nickname: String? = "Johnny"

val length = nickname?.length   // returns 6 if not null, returns null if null

Think of ?. as saying: "If this is not null, do this. Otherwise just give me null."

var nickname: String? = null
val length = nickname?.length   // returns null — no crash

Chaining safe calls:

You can chain multiple safe calls together:

data class User(val address: Address?)
data class Address(val city: City?)
data class City(val name: String?)

val user: User? = getUser()

// Without safe calls — crashes if anything is null
val cityName = user.address.city.name  // ❌ dangerous

// With safe calls — returns null if anything in the chain is null
val cityName = user?.address?.city?.name  // ✅ safe

Practical Android example:

class UserViewModel : ViewModel() {

    private var currentUser: User? = null

    fun getUserEmail(): String? {
        return currentUser?.email   // null if currentUser is null
    }

    fun getUserNameLength(): Int? {
        return currentUser?.name?.length  // null if user or name is null
    }
}

2. Elvis Operator — ?:

The Elvis operator ?: provides a default value when the left side is null. It's called Elvis because ?: looks like Elvis Presley's hair from the side 🕺

val nickname: String? = null
val length = nickname?.length ?: 0   // if null, use 0 instead

Think of ?: as saying: "If the left side is null, use the right side instead."

val name: String? = null
val displayName = name ?: "Guest"    // "Guest" if name is null

val age: Int? = null
val displayAge = age ?: 0            // 0 if age is null

val email: String? = getUserEmail()
val safeEmail = email ?: "no-email@default.com"

Combining with safe call:

This combination is the most common pattern in Kotlin:

val user: User? = getUser()

val name = user?.name ?: "Unknown"
val age = user?.age ?: 0
val email = user?.email ?: "Not provided"

Elvis with early return:

The right side of ?: can also be a return or throw:

fun processUser(user: User?) {
    val name = user?.name ?: return   // exit the function if name is null
    val email = user?.email ?: throw IllegalArgumentException("Email required")

    // Here we know name and email are not null
    sendWelcomeEmail(name, email)
}

Practical Android example:

fun displayUserProfile(user: User?) {
    nameTextView.text = user?.name ?: "Anonymous"
    ageTextView.text = user?.age?.toString() ?: "Age unknown"
    bioTextView.text = user?.bio ?: "No bio available"
    avatarUrl = user?.profileImageUrl ?: DEFAULT_AVATAR_URL
}

3. Not-Null Assertion — !!

The !! operator tells Kotlin: "I am 100% sure this is not null — trust me." If you're wrong and the value IS null, it throws a NullPointerException at runtime.

val name: String? = "John"
val length = name!!.length   // works if name is not null
val name: String? = null
val length = name!!.length   // ❌ crashes — NullPointerException

When is !! acceptable?

Rarely. Only use !! when:

  • You have already checked for null earlier and are logically certain it's not null
  • You are in test code
  • You are interfacing with Java code that guarantees non-null but Kotlin can't know that
// Acceptable — we just checked it ourselves
val name: String? = getName()
if (name != null) {
    val length = name.length  // smart cast — no !! needed here actually
}

// Acceptable in very specific cases where you know the value exists
val rootView = activity.window.decorView.rootView!!

In most cases, prefer ?. and ?: over !!.

// ❌ Avoid this pattern
val length = user!!.name!!.length

// ✅ Better
val length = user?.name?.length ?: 0

4. if null Check — Smart Cast

Kotlin is smart about null checks. When you check a nullable variable inside an if block, Kotlin automatically smart-casts it to the non-nullable type inside that block.

var name: String? = "John"

if (name != null) {
    // Inside here, Kotlin knows name is not null
    // It's automatically treated as String (not String?)
    println(name.length)   // ✅ no ?. needed here
    println(name.uppercase())
}

This also works with when:

val name: String? = getName()

when {
    name == null -> println("Name is null")
    name.isEmpty() -> println("Name is empty")    // smart cast here too
    else -> println("Name is: $name")
}

But smart cast doesn't work for var that could be changed by another thread:

var name: String? = "John"

if (name != null) {
    println(name.length)   // ❌ might not compile for var
    // another thread could set name = null between the check and .length
}

// Solution — use val or local variable
val localName = name
if (localName != null) {
    println(localName.length)  // ✅ smart cast works on val
}

// Or more idiomatically
name?.let { println(it.length) }

5. let — Run Code Only When Not Null

let is a scope function that runs a block of code only when the value is not null. It's perfect for performing an action on a nullable value.

val name: String? = "John"

name?.let {
    // 'it' refers to name, guaranteed non-null inside here
    println(it.length)
    println(it.uppercase())
    sendGreeting(it)
}
// If name is null, the entire block is skipped

Practical example:

val user: User? = getLoggedInUser()

user?.let { u ->
    // Only runs if user is not null
    displayName(u.name)
    loadAvatar(u.profileImageUrl)
    trackLogin(u.id)
}

Lateinit — Delayed Initialization

Sometimes you know a variable will be assigned before it's used, but you can't assign it at declaration time — like a View that gets set in onCreate().

class MainActivity : AppCompatActivity() {

    lateinit var nameTextView: TextView   // will be set in onCreate

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        nameTextView = findViewById(R.id.nameTextView)  // assigned here
        nameTextView.text = "Hello!"                    // used here
    }
}

lateinit tells Kotlin: "I'll assign this before I use it — don't enforce initialization at declaration."

Rules for lateinit:

  • Only works with var (not val)
  • Only works with non-primitive types (not Int, Boolean, etc.)
  • Throws UninitializedPropertyAccessException if used before assignment

Check if lateinit is initialized:

if (::nameTextView.isInitialized) {
    nameTextView.text = "Safe to use"
}

Nullable Collections

Collections can also be nullable at two levels — the collection itself, or the elements inside:

// Nullable list — the list itself can be null
var nullableList: List<String>? = null

// List of nullable strings — list exists, but elements can be null
var listWithNulls: List<String?> = listOf("Alice", null, "Charlie")

// Both nullable
var nullableListWithNulls: List<String?>? = null

Filtering null values from a list:

val listWithNulls: List<String?> = listOf("Alice", null, "Bob", null, "Charlie")

val cleanList: List<String> = listWithNulls.filterNotNull()
// Result: ["Alice", "Bob", "Charlie"]

Real-World Android Example

Here's a complete example showing how null safety works in a real Android ViewModel:

data class UserProfile(
    val id: String,
    val name: String,
    val email: String?,           // email is optional
    val bio: String?,             // bio is optional
    val profileImageUrl: String?, // image is optional
    val age: Int?                 // age is optional
)

class ProfileViewModel : ViewModel() {

    private var userProfile: UserProfile? = null

    fun loadProfile(userId: String) {
        viewModelScope.launch {
            userProfile = withContext(Dispatchers.IO) {
                repository.getUserProfile(userId)
            }
            updateUI()
        }
    }

    private fun updateUI() {
        val profile = userProfile ?: return  // exit if null

        // Safe access with defaults
        val displayName = profile.name                        // always exists
        val displayEmail = profile.email ?: "Email not set"  // default if null
        val displayBio = profile.bio ?: "No bio available"   // default if null
        val displayAge = profile.age?.toString() ?: "Age hidden"

        // Safe action — only runs if image URL exists
        profile.profileImageUrl?.let { url ->
            loadImage(url)
        }

        // Update UI
        _uiState.value = ProfileUiState(
            name = displayName,
            email = displayEmail,
            bio = displayBio,
            age = displayAge
        )
    }
}

Common Mistakes to Avoid

Mistake 1: Overusing !!

// ❌ Dangerous — one null causes a crash
val name = user!!.profile!!.name!!

// ✅ Safe — handles null gracefully
val name = user?.profile?.name ?: "Unknown"

Mistake 2: Ignoring the safe call result

val user: User? = getUser()

// ❌ You're not handling the null case
user?.updateProfile()  // this runs, but what if it didn't?

// ✅ Handle both cases when needed
if (user != null) {
    user.updateProfile()
    showSuccess()
} else {
    showLoginPrompt()
}

Mistake 3: Making everything nullable unnecessarily

// ❌ Don't make things nullable if they don't need to be
var name: String? = "John"  // will this ever be null? If not, don't use ?

// ✅ Only use ? when null is a real possibility
var name: String = "John"

Mistake 4: Using lateinit for primitives

// ❌ Won't compile — lateinit doesn't work with primitives
lateinit var count: Int

// ✅ Use nullable or provide an initial value instead
var count: Int = 0
var count: Int? = null

Quick Reference

Operator Name Meaning
String? Nullable type This variable can hold null
?. Safe call Call only if not null, return null otherwise
?: Elvis operator Use default value if null
!! Not-null assertion Force non-null, crash if null
let Scope function Run block only if not null
lateinit Late initialization Assign before first use
is Type check + smart cast Check type, auto-cast inside block

Summary

  • By default, no variable in Kotlin can be null — you must explicitly opt in with ?
  • Use ?. (safe call) to access properties and functions on nullable values without crashing
  • Use ?: (Elvis) to provide a default value when something is null
  • Avoid !! — it defeats null safety. Use it only when absolutely certain
  • Kotlin smart-casts variables to non-nullable inside if (x != null) blocks
  • Use let to run a block of code only when a value is not null
  • Use lateinit for variables that can't be initialized at declaration but will always be set before use
  • Use filterNotNull() to clean null values out of collections

Kotlin's null safety might feel restrictive at first — but within a week you'll realize it's actually liberating. You stop writing defensive null checks everywhere and start trusting your code to be correct.

Happy coding!