Kotlin bakes null handling into the type system, so the NullPointerException that plagues Java becomes a compile-time concern. Learn nullable types and the operators — ?., ?:, !! — that handle them safely.
Kotlin’s headline feature: a normal type can never hold null, and you opt into nullability with a trailing question mark. The compiler then forces you to handle the null case before using the value, turning the runtime NullPointerException — Tony Hoare’s "billion-dollar mistake" — into a compile-time error you cannot forget. This single design choice eliminates a huge class of crashes.
fun main() {
var name: String = "Ada"
// name = null // COMPILE ERROR: String can't be null
var maybe: String? = "Bo" // the '?' makes it nullable
maybe = null // now allowed
// println(maybe.length) // COMPILE ERROR: maybe could be null —
// the compiler won't let you deref it unsafely
}
// non-null by default; opt into null with '?'. The compiler enforces the rest.To use a nullable value you reach for two operators. The safe call ?. returns null instead of crashing if the receiver is null, and the Elvis operator ?: supplies a fallback when the left side is null. Chaining them lets you navigate possibly-null data and provide defaults in one readable line — the idiomatic way to handle nulls in Kotlin.
fun greet(name: String?): String {
val safeLength = name?.length // ?. -> Int? (null if name is null)
val length = name?.length ?: 0 // ?: -> fallback when null -> Int
val display = name ?: "guest" // default value
return "Hello $display (len ${name?.length ?: 0})"
}
fun main() {
println(greet("Ada")) // Hello Ada (len 3)
println(greet(null)) // Hello guest (len 0)
}
// name?.length?.plus(1) ?: 0 chains safe calls, then defaults.Two more tools round out null handling. The let scope function runs a block only when a value is non-null, giving you a non-null name inside. And Kotlin’s smart casts mean that once you check a value is not null (or is a certain type), the compiler treats it as such for the rest of the scope — no explicit cast needed. Together they make null-safe code concise.
fun printLength(name: String?) {
// run the block ONLY if name is non-null; 'it' is the non-null value
name?.let {
println("Length is ${it.length}")
}
// smart cast: after this null check, the compiler KNOWS name is non-null
if (name != null) {
println(name.length) // no ?. needed — smart-cast to String
}
}
fun main() { printLength("Ada"); printLength(null) } // prints for "Ada" onlyThe not-null assertion !! tells the compiler "trust me, this is not null," throwing a NullPointerException if you are wrong. It exists as an escape hatch but defeats the purpose of null safety, so treat it as a code smell — a signal that you should restructure to handle the null properly. Reaching for !! often means you have not modeled nullability correctly.
Avoid !! — The !! operator reintroduces the NullPointerException risk Kotlin is designed to prevent. Prefer ?., ?:, or let; reserve !! for the rare case where null is truly impossible and provably so.fun risky(name: String?) {
val length = name!!.length // throws NPE if name is null — the escape hatch
println(length)
}
// !! reintroduces the exact crash Kotlin prevents. Prefer:
// name?.length ?: 0 (safe call + default)
// name?.let { ... } (run only if non-null)
// if (name != null) { ... } (smart cast)
// If you're reaching for !!, the nullability is usually modeled wrong.