Null Safety
Master Kotlin's null-safe type system: nullable types, the safe call operator, the Elvis operator, safe casts, and when to use !!.
Introduction
Null safety is the feature Kotlin is most famous for, and the single biggest reason many teams migrate from Java. Tony Hoare, who invented the null reference in 1965, later called it his "billion-dollar mistake" because of how many production crashes it has caused across the software industry. Kotlin's type system tackles this head-on by making nullability an explicit, checked part of every type.
This lesson covers nullable types, the operators Kotlin gives you to work with them safely (`?.`, `?:`, `as?`), the not-null assertion `!!` and why to use it sparingly, and how smart casts eliminate manual null checks after you've already proven a value isn't null.
- How nullable types (`String?`) differ from non-nullable types (`String`).
- How to safely access properties on a possibly-null value with `?.`.
- How to provide a fallback value with the Elvis operator `?:`.
- What the not-null assertion `!!` does, and why overusing it defeats the purpose of null safety.
- How smart casts let the compiler treat a checked nullable value as non-null automatically.
Nullable vs Non-Nullable Types
Every type in Kotlin is non-nullable by default. To allow a variable to hold `null`, you must explicitly mark its type with a trailing question mark, e.g. `String?` instead of `String`. The compiler then tracks this distinction everywhere and refuses to compile code that could dereference a nullable value without first checking it.
fun main() { var name: String = "Kotlin" // name = null // Compile error: String cannot hold null
var nickname: String? = "Kt" nickname = null // OK — String? can hold null
// println(nickname.length) // Compile error: nickname might be null println(nickname?.length) // OK — handled safely, prints "null"}Click Run to see what this code prints.
The Safe Call Operator ?.
The safe call operator `?.` accesses a property or calls a method only if the value on its left is not null; otherwise, the entire expression short-circuits to `null` instead of throwing. It can also be chained across multiple nullable steps.
class Address(val city: String?)class Customer(val address: Address?)
fun main() { val customer: Customer? = Customer(Address("Mumbai")) val noAddress: Customer? = Customer(null)
println(customer?.address?.city) // Mumbai println(noAddress?.address?.city) // null — no crash}Click Run to see what this code prints.
The Elvis Operator ?:
The Elvis operator `?:` provides a fallback value to use when the expression on its left is null. It is commonly chained right after a safe call, giving you a one-line "use this value, or a default if it's null" pattern.
fun main() { val nickname: String? = null val displayName = nickname ?: "Anonymous" println(displayName)
val length = nickname?.length ?: 0 println(length)}Click Run to see what this code prints.
The Elvis operator gets its name from the `?:` symbol, which — turned sideways — resembles Elvis Presley's hairstyle and eyes. It can also be used with `return` or `throw` on the right-hand side to exit a function early when a value is unexpectedly null.
The Not-Null Assertion !!
The `!!` operator forcibly converts a nullable type to its non-null counterpart, telling the compiler "trust me, this is definitely not null." If you are wrong, it throws a `NullPointerException` immediately — exactly the exception Kotlin's null safety exists to prevent.
fun main() { val name: String? = "Kotlin" val length: Int = name!!.length // asserts name is not null println(length)
val missing: String? = null // val crash = missing!!.length // throws NullPointerException at runtime}Click Run to see what this code prints.
Every `!!` in your code is a place you've manually opted back into the exact crash Kotlin's type system is designed to prevent at compile time. It has legitimate uses (e.g. values you've already validated elsewhere), but reaching for it reflexively instead of a safe call or Elvis operator is one of the most common mistakes Java developers make when learning Kotlin.
Safe Casts with as?
The regular `as` cast throws a `ClassCastException` if the value isn't the target type. The safe cast operator `as?` instead returns `null` if the cast fails, letting you handle the mismatch gracefully instead of crashing.
fun main() { val value: Any = "Hello" val asInt: Int? = value as? Int // fails safely — value isn't an Int val asString: String? = value as? String
println(asInt) println(asString)}Click Run to see what this code prints.
Smart Casts After Null Checks
Once the compiler can prove a nullable value isn't null within a certain scope — for example, inside an `if (value != null)` block — it automatically treats that value as its non-null type for the rest of that scope. This is called a smart cast, and it means you rarely need `!!` after an explicit null check.
fun printLength(text: String?) { if (text != null) { println(text.length) // smart-cast: text is treated as String here, no !! needed } else { println("No text provided") }}
fun main() { printLength("Kotlin") printLength(null)}Click Run to see what this code prints.
lateinit and Platform Types
Sometimes a non-null property genuinely can't be initialized in the constructor (a common example: Android views set up later in a lifecycle method). The `lateinit` modifier lets you declare a non-nullable `var` without an initial value, promising to set it before first use — accessing it too early throws a clear `UninitializedPropertyAccessException` rather than silently allowing null.
When Kotlin code calls into Java code, values coming from Java have no compile-time nullability information (Java has no `?` syntax), so Kotlin treats them as "platform types," shown as `String!`. You can treat a platform type as either nullable or non-nullable, but the compiler cannot protect you there the way it can with pure Kotlin code — so it is worth being extra careful at Java interop boundaries.
class UserSession { lateinit var username: String
fun login(name: String) { username = name }}
fun main() { val session = UserSession() session.login("Elena") println(session.username)}Click Run to see what this code prints.
Common Mistakes
- Overusing `!!` as a shortcut to silence compiler nullability warnings instead of actually handling the null case.
- Forgetting a smart cast is invalidated if the compiler cannot guarantee the value won't change between the check and the use (e.g. a mutable property another thread could modify).
- Accessing a `lateinit` property before it has been initialized, causing an `UninitializedPropertyAccessException`.
- Treating a Java platform type as automatically safe — Java code still can, and often does, hand back genuine nulls.
Best Practices
- Default to `?.` and `?:` for handling nullable values; reserve `!!` for cases you have already proven are safe by other means.
- Push nullability decisions to the boundary of your program (e.g. parsing input, reading from a database) rather than letting `?` types spread everywhere.
- Prefer smart casts over manual casting — an early `if (x != null)` check often removes the need for any operator at all afterward.
- Use `lateinit` only for properties you are certain will be set before use, and only for non-primitive types (it doesn't support Int, Boolean, etc. directly).
Frequently Asked Questions
It eliminates the vast majority of them by catching mistakes at compile time, but not 100% — using `!!` incorrectly, or interacting with Java code that returns unexpected nulls, can still cause one at runtime.
`?.` safely handles a possible null by short-circuiting to null; `!!` asserts the value is definitely not null and crashes immediately if that assertion is wrong. `?.` is defensive; `!!` is a bet.
Rarely — mainly when you have external knowledge the compiler cannot infer (e.g. you just checked a map contains a key in a way the compiler cannot track). Even then, a safe call with a clear fallback is usually preferable.
Key Takeaways
- Types are non-nullable by default in Kotlin; append `?` to explicitly allow null.
- The safe call operator `?.` short-circuits to null instead of crashing on a null receiver.
- The Elvis operator `?:` supplies a fallback value when the left-hand expression is null.
- The not-null assertion `!!` reintroduces the exact NullPointerException risk Kotlin is designed to prevent — use it sparingly.
- Smart casts let the compiler treat an already-checked nullable value as non-null for the rest of that scope.
Summary
You now understand Kotlin's null-safe type system in depth — its defining feature. Next, you'll put this together with Kotlin's built-in collection types: List, Set, and Map.