In the previous tutorial, you learned about variables and types. Now let’s learn about null safety — the feature that makes Kotlin truly special.
NullPointerException (NPE) is the most common crash in Java and many other languages. Tony Hoare, who invented null references in 1965, called it his “billion-dollar mistake”. Kotlin solves this problem at compile time.
In this tutorial, you will learn:
- Why null is dangerous
- How Kotlin prevents null problems
- All null safety operators:
?,?.,?:,!! - The
letfunction for null handling - Smart casts
- Safe casting with
as? lateinitfor late initialization
The Problem with Null
In Java, any reference type can be null:
// Java — this compiles fine
String name = null;
int length = name.length(); // CRASH: NullPointerException at runtime
The program compiles without errors. But it crashes when you run it. This is dangerous because:
- The compiler does not warn you
- The crash happens at runtime, possibly in production
- It is hard to find all places where null could appear
Kotlin’s Solution: Nullable Types
In Kotlin, variables cannot be null by default:
// This does NOT compile
val name: String = null // Error: Null cannot be a value of a non-null type String
If you want a variable to be nullable, you must add ? to the type:
// This compiles — ? means "this can be null"
val name: String? = null
val email: String? = "alex@example.com"
Think of String and String? as two different types:
String— always has a value, never nullString?— might have a value, might be null
The compiler knows the difference and forces you to handle null.
Safe Call Operator: ?.
The safe call operator ?. calls a method only if the value is not null. If the value is null, it returns null instead of crashing.
val name: String? = "Alex"
val nickname: String? = null
println(name?.length) // 4
println(nickname?.length) // null (not a crash!)
You can chain safe calls:
println(name?.uppercase()) // ALEX
println(nickname?.uppercase()) // null
Multiple safe calls in a chain:
val city: String? = "Berlin"
println(city?.first()?.uppercase()) // B
If any value in the chain is null, the entire expression returns null.
Elvis Operator: ?:
The Elvis operator ?: provides a default value when the expression is null. The name comes from Elvis Presley — ?: looks like his hairstyle if you turn it sideways.
val name: String? = null
// If name is null, use "Unknown"
val displayName = name ?: "Unknown"
println(displayName) // Unknown
When the value is not null, the Elvis operator returns the original value:
val email: String? = "sam@example.com"
val displayEmail = email ?: "no email"
println(displayEmail) // sam@example.com
Combine safe call with Elvis for a common pattern:
val name: String? = null
val length = name?.length ?: 0
println(length) // 0
Elvis with Return
You can use Elvis to exit a function early:
fun processInput(input: String?) {
val text = input ?: return // Exit if input is null
println("Processing: $text")
}
Elvis with Throw
You can use Elvis to throw an exception:
fun getRequiredName(name: String?): String {
return name ?: throw IllegalArgumentException("Name is required")
}
Not-Null Assertion: !!
The !! operator tells the compiler: “I am sure this is not null.” It converts a nullable type to a non-null type.
val name: String? = "Jordan"
val length = name!!.length // 6
Warning: If the value IS null, !! throws NullPointerException:
val name: String? = null
val length = name!!.length // CRASH: NullPointerException
Avoid !! in production code. It defeats the purpose of null safety. Use ?. or ?: instead. The only time !! is acceptable is when you are absolutely certain the value cannot be null and the compiler cannot figure it out.
The let Function
let is a function that runs a block of code on a value. When combined with ?., it runs the block only if the value is not null.
val email: String? = "alex@example.com"
val phone: String? = null
// Runs the block because email is not null
email?.let {
println("Sending email to: $it")
println("Email length: ${it.length}")
}
// Does NOT run the block because phone is null
phone?.let {
println("This will not print")
}
You can name the parameter instead of using it:
val name: String? = "Sam"
name?.let { nonNullName ->
println("Name: $nonNullName")
println("Uppercase: ${nonNullName.uppercase()}")
println("Length: ${nonNullName.length}")
}
Inside the let block, the value is guaranteed to be non-null. This is safer than doing a null check with if.
Smart Casts
After a null check, Kotlin automatically “smart casts” the variable to a non-null type:
val name: String? = "Alex"
if (name != null) {
// Kotlin knows name is non-null here — no ?. needed
println("Name length: ${name.length}")
println("Uppercase: ${name.uppercase()}")
}
Smart casts also work with type checks:
val value: Any? = "Hello"
when (value) {
is String -> println("String with length ${value.length}")
is Int -> println("Int with value $value")
null -> println("It's null")
else -> println("Unknown type")
}
After is String, Kotlin smart casts value to String, so you can call .length without any cast.
Safe Casting: as?
Regular casting with as crashes if the type is wrong:
val value: Any = "Hello"
val number: Int = value as Int // CRASH: ClassCastException
Safe casting with as? returns null if the cast fails:
val value: Any = "Hello"
val number: Int? = value as? Int
println(number) // null (no crash)
val str: String? = value as? String
println(str) // Hello
Combine with Elvis for a safe pattern:
val value: Any = "Hello"
val length = (value as? String)?.length ?: 0
println(length) // 5
lateinit
Sometimes you cannot initialize a variable right away, but you know it will be initialized before you use it. Use lateinit for this:
class UserProfile {
lateinit var name: String
lateinit var email: String
fun initialize(name: String, email: String) {
this.name = name
this.email = email
}
fun printProfile() {
if (::name.isInitialized) {
println("Name: $name, Email: $email")
} else {
println("Profile not initialized yet")
}
}
}
Rules for lateinit:
- Only works with
var, notval - Only works with non-primitive types (not
Int,Boolean, etc.) - Must be initialized before first use, or you get
UninitializedPropertyAccessException - Use
::property.isInitializedto check if it was initialized
val profile = UserProfile()
profile.printProfile() // Profile not initialized yet
profile.initialize("Alex", "alex@example.com")
profile.printProfile() // Name: Alex, Email: alex@example.com
Practical Patterns
Pattern 1: Find or Default
val users = listOf("Alex", "Sam", "Jordan")
val found = users.find { it == "Sam" } ?: "Not found"
val notFound = users.find { it == "Taylor" } ?: "Not found"
println(found) // Sam
println(notFound) // Not found
Pattern 2: Chain of Safe Calls
data class Address(val city: String?, val country: String?)
data class Person(val name: String, val address: Address?)
val person = Person("Alex", Address("Berlin", "Germany"))
val noAddress = Person("Sam", null)
println(person.address?.city?.uppercase()) // BERLIN
println(noAddress.address?.city?.uppercase()) // null
Pattern 3: Default Values for Nullable Properties
val city = person.address?.city ?: "Unknown city"
val city2 = noAddress.address?.city ?: "Unknown city"
println(city) // Berlin
println(city2) // Unknown city
Pattern 4: Filter Out Nulls
val list: List<String?> = listOf("Alex", null, "Sam", null, "Jordan")
val nonNullList: List<String> = list.filterNotNull()
println(nonNullList) // [Alex, Sam, Jordan]
Pattern 5: Map with Nullable Values
val scores: Map<String, Int?> = mapOf(
"Alex" to 95,
"Sam" to null,
"Jordan" to 87
)
for ((name, score) in scores) {
println("$name: ${score ?: "No score"}")
}
// Alex: 95
// Sam: No score
// Jordan: 87
Null Safety Cheat Sheet
| Operator | Name | What It Does |
|---|---|---|
? | Nullable type | Allows null: String? |
?. | Safe call | Calls method if not null, returns null otherwise |
?: | Elvis | Provides default value if null |
!! | Not-null assertion | Converts to non-null, crashes if null |
as? | Safe cast | Returns null if cast fails |
?.let {} | Null-safe let | Runs block only if not null |
Rules for Null Safety
- Use
valand non-null types by default. Only use nullable types when you truly need them. - Use
?.and?:for null handling. They are safe and readable. - Avoid
!!in production code. It is a sign that you should refactor. - Use
letfor complex null handling. It keeps the non-null value in scope. - Use
lateinitonly when necessary. Prefer initializing in the constructor.
Source Code
You can find the complete source code for this tutorial on GitHub:
What’s Next?
In the next tutorial, Kotlin Tutorial #5: Functions, Default Parameters, and Named Arguments, you will learn:
- How to write functions in Kotlin
- Single-expression functions
- Default parameters
- Named arguments
- Varargs
- The
Unitreturn type
Functions in Kotlin are more powerful than in Java. See you there.
This is part 4 of the Kotlin Tutorial series. Check out Part 3: Variables and Types if you missed it. Need a quick reference? See the Kotlin Cheat Sheet.