Kotlin Null Safety Cheat Sheet
Covers nullable types, the safe call and elvis operators, the not-null assertion, and platform types for handling null safely in Kotlin.
Nullable Types
The ? suffix marks a type as allowed to hold null.
var name: String = "Kotlin" // non-null, cannot be assigned null// name = null // compile errorvar nickname: String? = "Kt" // nullable, marked with ?nickname = null // OKfun greet(name: String) { // parameter cannot be null println("Hello, $name")}fun greetSafe(name: String?) { // parameter may be null println("Hello, ${name ?: "Guest"}")}
Safe Calls & Elvis Operator
Chaining nullable access without throwing.
val nickname: String? = null// Safe call: returns null instead of throwing if the receiver is nullval length: Int? = nickname?.length// Elvis operator: provide a default when the left side is nullval displayName: String = nickname ?: "Anonymous"// Chaining safe callsdata class Address(val city: String?)data class User(val address: Address?)val user: User? = User(Address(null))val city = user?.address?.city ?: "Unknown"// Elvis with early return/throwfun process(value: String?) { val v = value ?: return println(v.uppercase())}
Not-Null Assertion
Forcing an unwrap when you're certain a value is non-null.
val nickname: String? = "Kt"// !! throws NullPointerException if the value is actually nullval length: Int = nickname!!.length// Use sparingly -- only when you are certain the value cannot be null// and a crash is the correct behavior if that assumption is wrong.fun getConfig(): String? = readConfigFile()// Prefer requireNotNull/checkNotNull for clearer failure messagesval config: String = requireNotNull(getConfig()) { "Config must be present" }
Null Safety Operators
The full operator toolkit for working with nullable types.
- ? (nullable marker)- Declares that a type may hold null, e.g. String?
- ?. (safe call)- Calls a member only if the receiver is non-null; otherwise evaluates to null
- ?: (elvis operator)- Supplies a default value when the left-hand expression is null
- !! (not-null assertion)- Forces unwrap; throws NullPointerException if the value is null
- ?.let { }- Executes the block only when the receiver is non-null, using it as the argument
- as? (safe cast)- Casts to a type, returning null instead of throwing ClassCastException on failure
- lateinit var- Defers initialization of a non-null var, avoiding a nullable type for late-bound properties
Smart Casts
The compiler automatically treats a checked nullable as non-null.
fun describe(value: String?) { if (value != null) { // Kotlin smart-casts value to non-null String inside this block println(value.length) } // Also works with early return if (value == null) return println(value.uppercase())}// Smart casts don't work across mutable var properties that could change,// but work reliably with local val and immutable properties.
Platform Types from Java Interop
Types coming from Java have no compile-time null information and are treated as neither nullable nor non-null.
// Java method: public String getName() { return null; } // no @Nullable annotation// Kotlin sees getName() as a "platform type" String! (unofficial notation)val name = javaObject.getName() // compiles as if non-null, but can still be null// Two failure modes if the Java side actually returns null:val length = name.length // NPE happens HERE, at the point of use, not assignment// Defensive interop: treat platform types as nullable explicitlyval safeName: String? = javaObject.getName()val safeLength = safeName?.length ?: 0// Best practice: wrap Java APIs at the boundary with explicit nullabilityfun getUserNameSafely(java: JavaUser): String = java.getName() ?: ""// @NonNull / @Nullable annotations on the Java side make Kotlin enforce them properly
Custom Null-Check Functions with Contracts
Kotlin's contracts API lets your own helper functions participate in smart casting.
import kotlin.contracts.ExperimentalContractsimport kotlin.contracts.contract@OptIn(ExperimentalContracts::class)fun isValidEmail(value: String?): Boolean { contract { returns(true) implies (value != null) } return !value.isNullOrBlank() && value.contains("@")}fun send(email: String?) { if (isValidEmail(email)) { // compiler smart-casts email to non-null String here because of the contract println(email.length) }}// Standard library examples already using contracts:val s: String? = nullif (s.isNullOrEmpty()) { // s is smart-cast-eligible as null OR empty inside this branch} else { println(s.length) // s smart-cast to non-null String}
Nullability with Generics & Collections
Distinguishing a nullable list from a list of nullable elements.
val listOfNullableInts: List<Int?> = listOf(1, null, 3) // elements may be nullval nullableListOfInts: List<Int>? = null // the list reference itself may be null// filterNotNull() drops nulls and returns List<Int>val nonNullInts: List<Int> = listOfNullableInts.filterNotNull()// mapNotNull combines map + filter out nulls in one passval doubled = listOfNullableInts.mapNotNull { it?.times(2) }// firstOrNull / find return null instead of throwing NoSuchElementExceptionval first: Int? = listOfNullableInts.firstOrNull { it != null && it > 1 }// getOrNull on arrays/lists avoids IndexOutOfBoundsExceptionval arr = arrayOf(1, 2, 3)val fourth: Int? = arr.getOrNull(10) // null, not a crash// generic type parameters are non-null by default; use T? to opt into nullabilityfun <T : Any> firstNotNull(list: List<T?>): T? = list.firstOrNull { it != null }
lateinit vs by lazy vs Delegates.notNull
Three strategies for deferring initialization of a non-null property.
import kotlin.properties.Delegatesclass Config { // lateinit: for var, must be set before first read, throws // UninitializedPropertyAccessException if read too early. No primitives. lateinit var apiKey: String // by lazy: for val, computed once on first access, thread-safe by default val expensiveValue: String by lazy { println("computing...") loadFromDisk() } // Delegates.notNull: like lateinit but works with primitives (Int, Boolean, ...) var retryCount: Int by Delegates.notNull() fun isApiKeyReady() = ::apiKey.isInitialized // check without triggering the exception}fun loadFromDisk(): String = "cached-value"
Common Null-Safety Pitfalls
Situations where Kotlin's null safety is easy to accidentally defeat.
- Smart cast fails on var- A mutable property can change between the null check and use, especially across threads or lambdas
- !! in chained calls- a!!.b!!.c!! hides which link actually threw; prefer one guarded unwrap with a message
- Platform types from Java- No compiler enforcement; treat any un-annotated Java return value as nullable
- equals() on nullable receivers- a == b is null-safe in Kotlin, but a.equals(b) throws if a is null
- Nullable var captured in a lambda- The compiler can't guarantee it wasn't mutated by the time the lambda runs
- Reflection / deserialization- JSON libraries can bypass the type system and set a non-null val to null via reflection
Avoid `!!` in application code — it reintroduces the exact NullPointerException risk Kotlin's type system is designed to eliminate. Prefer `?.`, `?:`, or `requireNotNull()` with a descriptive message so failures are self-explanatory.