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, andalsowith 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"
.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
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"
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
const name = null;
const display = name ?? "Unknown"; // nullish coalescing
const length = name?.length ?? 0; // optional chaining + ??
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
!!, 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
}
}
var properties that could be changed by another thread.The compiler only smart-casts:
โข
val local variablesโข
val properties that can't be overriddenFor
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"
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"
let too deeply โ flatten with ?: return or early returns.
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.
| Type | Meaning | Example |
|---|---|---|
List<String> | Non-null list of non-null strings | The list exists, every item exists |
List<String>? | Nullable list of non-null strings | The list itself might be null |
List<String?> | Non-null list of nullable strings | The list exists, but some items are null |
List<String?>? | Nullable list of nullable strings | Everything 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"
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)
}
| Function | Context object | Returns | Use with nulls |
|---|---|---|---|
let | it | Lambda result | name?.let { } |
run | this | Lambda result | person?.run { } |
with | this | Lambda result | with(person) { } (non-null) |
apply | this | Context object | button?.apply { } |
also | it | Context object | person?.also { } |
โข
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
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]
๐ Official docs: Null Safety (kotlinlang.org) ยท Smart Casts (kotlinlang.org)
Try It Yourself
You have a nullable email string. Write a single expression that:
- Returns the email in lowercase if it's not null and contains
"@" - 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