Kotlin Crash Course
Chapter 3

Null Safety

Kotlin's crown jewel. Learn how the type system prevents billion-dollar mistakes by making nullability explicit and safe.

By the end of this chapter, you will:

  • Understand nullable (String?) vs non-nullable (String) types
  • Use the safe call operator ?. and Elvis operator ?:
  • Know when (and when not) to use !!
  • Handle nullable collections and chaining
  • Use let, run, and also with nullables

The Problem: Null is Dangerous

In JavaScript and Python, any variable can be null or None at any time. This leads to runtime crashes that are hard to predict.

// JavaScript
function getLength(name) {
    return name.length;  // ๐Ÿ’ฅ CRASH if name is null or undefined
}

getLength(null);  // TypeError: Cannot read properties of null
# Python
def get_length(name):
    return len(name)  # ๐Ÿ’ฅ CRASH if name is None

get_length(None)  # TypeError: object of type 'NoneType' has no len()

Kotlin solves this at the type system level. A variable either can be null (and the compiler forces you to handle it) or it cannot be null (and it never will be).

Nullable vs Non-Nullable Types

In Kotlin, types are non-nullable by default. To allow null, you add ? to the type.

var name: String = "Alex"
name = null           // โŒ COMPILE ERROR: Null can not be a value of a non-null type String

var nullableName: String? = "Alex"
nullableName = null   // โœ… OK โ€” String? means "String or null"
This is huge: The compiler knows which variables can be null. It won't let you call .length on a String? without first checking for null. This catches NPEs before you run the program.

Safe Call Operator: ?.

Call a method or access a property only if the receiver is not null. Otherwise, return null.

val name: String? = null

// Without safe call โ€” COMPILE ERROR:
// val length = name.length  // โŒ Only safe (?.) or non-null asserted (!!.) calls allowed

// With safe call:
val length: Int? = name?.length   // โœ… null โ€” because name is null

val realName: String? = "Alex"
val realLength: Int? = realName?.length   // โœ… 4

Chaining Safe Calls

data class Address(val city: String?)
data class Person(val address: Address?)

val person: Person? = null

// Chain of safe calls โ€” returns null if ANY link is null
val city: String? = person?.address?.city

// Without safe calls, this would be:
// val city = if (person != null && person.address != null) person.address.city else null
Practical use case: Navigating nested JSON models from an API โ€” user?.profile?.avatarUrl?.let { loadImage(it) } safely handles every missing level in a single chain.

Elvis Operator: ?:

Provide a default value when something is null. Named after Elvis Presley's hair (?: looks like his pompadour).

val name: String? = null

// If name is null, use "Unknown" instead
val displayName = name ?: "Unknown"   // "Unknown"

// Works with any expression
val length = name?.length ?: 0        // 0

// Can throw as default too
val critical = name ?: throw IllegalArgumentException("Name required!")

// Equivalent to:
// val displayName = if (name != null) name else "Unknown"
Practical use cases: Provide default configuration values (timeout ?: 5000), return early error messages (username ?: return "Username required"), or supply fallback images when a URL is missing in a news feed app.

Compare with JS / Python

โ–ผ
JavaScript
const name = null;
const display = name ?? "Unknown";     // nullish coalescing
const length = name?.length ?? 0;      // optional chaining + ??
Python
name = None
display = name or "Unknown"           # but or is truthy, not null-check
length = len(name) if name else 0     # verbose

Kotlin difference: Kotlin had ?. and ?: years before JS added ?. and ??. But Kotlin's version is type-safe โ€” the compiler tracks what can be null through the whole chain.

The !! Operator: Use With Caution

Converts a nullable type to non-nullable, crashing at runtime if it's actually null. Think of it as "I know better than the compiler."

val name: String? = null

val length = name!!.length   // ๐Ÿ’ฅ NullPointerException at runtime!

// Only use !! when you are 100% sure it's not null
// Better alternatives exist almost always
Rule of thumb: If you're using !!, you're probably doing something wrong. There is almost always a safer alternative: ?., ?:, let, or restructuring your code.

Safe Casts: as?

Cast a value to a type, returning null if the cast fails instead of throwing an exception.

val x: Any = "Hello"

val str: String? = x as? String     // "Hello"
val num: Int? = x as? Int           // null (no crash!)

// Without safe cast:
// val num = x as Int               // ๐Ÿ’ฅ ClassCastException

Smart Casts with when

Kotlin's compiler is smart enough to automatically cast a variable after an is check. This works especially well with when expressions for clean, type-specific handling.

fun describe(x: Any) = when (x) {
    is String -> "String of length ${x.length}"  // x smart-cast to String
    is Int -> "Integer: $x"                       // x smart-cast to Int
    is Double -> "Double: $x"                     // x smart-cast to Double
    else -> "Unknown type"
}

// Smart casts also work with && conditions
fun isValidString(x: Any): Boolean =
    x is String && x.length > 0  // x is smart-cast inside the condition

// Compiler tracks null checks across branches
fun greet(name: String?) {
    if (name != null) {
        println(name.uppercase())  // name is String here
    } else {
        println("No name")         // name is null here
    }
}
Practical use case: Smart casts are perfect for parsing JSON or API responses where a field might be a string, number, or boolean. Instead of manual casting, let the compiler do the work.
Limitation: Smart casts don't work on var properties that could be changed by another thread.

The compiler only smart-casts:
โ€ข val local variables
โ€ข val properties that can't be overridden

For var, use an explicit cast or assign to a local val first.

Platform Types

When calling Java code from Kotlin, types come back as platform types โ€” written with a ! suffix like String!. Kotlin doesn't know whether a Java method can return null, so it lets you assign the result to either a nullable or non-nullable variable.

// Calling a Java method: public String getName()
val name: String = javaObject.getName()        // โš  Allowed, but risky!
val safeName: String? = javaObject.getName()  // โœ… Safer โ€” treat as nullable

// Android example: findViewById returns a platform type
val button: Button = findViewById(R.id.btn)       // โš  Crashes if view missing
val safeButton: Button? = findViewById(R.id.btn)  // โœ… Safe โ€” check before use

// Best practice: treat platform types as nullable
val text = findViewById<TextView>(R.id.tv)?.text ?: "Default"
Rule of thumb: Always treat platform types as nullable unless you are 100% sure the Java method never returns null. In Android, views can be missing, so always use nullable types with findViewById and the safe call operator.

let with Nullables

Use let to execute a block of code only if a value is not null.

val name: String? = "Alex"

// Only runs if name is not null
name?.let { nonNullName ->
    println("Name is $nonNullName")
    println("Length is ${nonNullName.length}")
}

// Inside let, `it` refers to the non-null value
name?.let {
    println("Hello, $it")
}

// Chaining: compute only if all values exist
val result = name?.let { it.uppercase() } ?: "EMPTY"
When to use let: Perfect for transforming nullable values, doing multiple operations on a nullable, or early-return patterns. Avoid nesting let too deeply โ€” flatten with ?: return or early returns.
Practical use case: In an Android login flow, email?.let { validateFormat(it) } runs validation only when the user has entered text. Combine with ?: to show an error if null: email?.let { submit(it) } ?: showError("Enter email").

Nullable vs Non-Null Collections

Kotlin lets you express nullability at three levels: the collection itself, its elements, or both. This is crucial when working with APIs that may return missing data.

TypeMeaningExample
List<String>Non-null list of non-null stringsThe list exists, every item exists
List<String>?Nullable list of non-null stringsThe list itself might be null
List<String?>Non-null list of nullable stringsThe list exists, but some items are null
List<String?>?Nullable list of nullable stringsEverything can be null
// API response: list might be null, items might be null
val apiResponse: List<String?>? = fetchTags()

// Safe access: only iterate if list is not null, filter out null items
val validTags = apiResponse?.filterNotNull() ?: emptyList()

// Chaining safe calls on nullable elements
val firstLength = apiResponse?.firstOrNull()?.length  // Int?

// filterNotNull() converts List<String?> to List<String>
val nullableList: List<String?> = listOf("a", null, "c")
val nonNullList: List<String> = nullableList.filterNotNull()

// Map with nullable values
val map: Map<String, String?> = mapOf("key" to null)
val value = map["key"] ?: "missing"
Practical use case: JSON APIs often return arrays where the array itself or individual elements may be missing. Using List<String?>? lets you model this precisely and handle it safely with a single chain of ?.filterNotNull().

Scope Functions: also, apply, run, let, with

Kotlin provides five scope functions to execute blocks of code on an object. They differ in what they return and how they reference the object. All of them work seamlessly with nullables.

// also โ€” side effects, returns the original object
val person = Person("Alex").also {
    println("Created: $it")
}

// apply โ€” object configuration, returns the original object
val button = Button(context).apply {
    text = "OK"
    setOnClickListener { /* ... */ }
}

// run โ€” transformation + result, needs a receiver
val greeting = person?.run {
    "$name is $age years old"
}

// let โ€” transformation to another type, perfect with nullables
val result = email?.let { sendEmail(it) }

// with โ€” non-null context block
with(person) {
    println(name)
    println(age)
}
FunctionContext objectReturnsUse with nulls
letitLambda resultname?.let { }
runthisLambda resultperson?.run { }
withthisLambda resultwith(person) { } (non-null)
applythisContext objectbutton?.apply { }
alsoitContext objectperson?.also { }
When to use which:
โ€ข let โ€” null-safe transformations & mapping
โ€ข apply โ€” builder-pattern object setup
โ€ข also โ€” logging or side effects without changing the value
โ€ข run โ€” compute a result from a nullable object
โ€ข with โ€” operate on a non-null object concisely
Practical use case: In Android:
findViewById<TextView>(R.id.title)?.apply { text = "Hello"; textSize = 18f }
Finds a view, configures it, and returns it for chaining โ€” all in one null-safe expression.
// Safe cast with as? โ€” returns null if cast fails
val maybeString: Any = 42
val str = maybeString as? String   // null, no crash

// Filter nulls from a list of nullable values
val names: List<String?> = listOf("A", null, "B", null, "C")
val nonNullNames = names.filterNotNull()  // ["A", "B", "C"]

// Map with nullable transform โ€” returns List<Int?>
val lengths = names.map { it?.length }   // [1, null, 1, null, 1]

Try It Yourself

You have a nullable email string. Write a single expression that:

  1. Returns the email in lowercase if it's not null and contains "@"
  2. Returns "invalid" otherwise
val email: String? = "ALEX@EXAMPLE.COM"

// Your single expression here:
val result = 

println(result)  // Should print: alex@example.com
Stuck? Show hint
email?.takeIf { it.contains("@") }?.lowercase() ?: "invalid"

Concept Check

What is the type of result?

val name: String? = "Alex"
val result = name?.length
Int
Int?
String?