| name | Kotlin Null Safety |
| user-invocable | false |
| description | Use when kotlin's null safety system including nullable types, safe calls, Elvis operator, smart casts, and patterns for eliminating NullPointerExceptions while maintaining code expressiveness and clarity. |
| allowed-tools | [] |
Kotlin Null Safety
Introduction
Kotlin's null safety system eliminates NullPointerExceptions at compile time by
distinguishing between nullable and non-nullable types in the type system. This
approach makes null handling explicit and forces developers to consciously
handle potential null values.
Unlike Java where any reference can be null, Kotlin requires explicit
declaration of nullability with the ? operator. The compiler enforces null
checks before dereferencing nullable values, preventing the vast majority of
null-related crashes that plague Java applications.
This skill covers nullable types, safe call operators, smart casts, nullability
in generic types, and patterns for designing null-safe APIs while maintaining
code clarity.
Nullable Types
Nullable types explicitly indicate that a variable or property can hold null,
while non-nullable types provide compile-time guarantees of non-null values.
var name: String = "Alice"
var nullableName: String? = "Bob"
nullableName = null
fun greet(name: String) {
println("Hello, $name")
}
fun greetNullable(name: String?) {
if (name != null) {
println("Hello, $name")
} else {
println("Hello, guest")
}
}
greetNullable(null)
fun findUser(id: Int): User? {
return if (id > 0) User(id, "Alice") else null
}
data class User(val id: Int, val name: String)
class Person(
val name: String,
val email: String?,
var phoneNumber: String?
)
val nullableList: List<String?> = listOf("A", null, "B")
val listOfNullable: List<String>? = null
class Service {
fun process() {
val self: Service? = this
self?.validate()
}
fun validate() {}
}
The ? suffix makes a type nullable. Non-nullable types cannot be assigned null
without explicit nullability declaration, preventing accidental null references.
Safe Call Operator
The safe call operator ?. safely accesses properties and methods on nullable
references, returning null if the receiver is null instead of throwing NPE.
val name: String? = "Alice"
val length: Int? = name?.length
val nullName: String? = null
val nullLength: Int? = nullName?.length
data class Address(val street: String?, val city: String?)
data class Company(val address: Address?)
data class Employee(val company: Company?)
val employee: Employee? = Employee(Company(Address("Main St", "NYC")))
val city: String? = employee?.company?.address?.city
println(city)
val nullEmployee: Employee? = null
val nullCity: String? = nullEmployee?.company?.address?.city
println(nullCity)
fun processUser(user: User?) {
user?.let { u ->
println("Processing ${u.name}")
}
}
fun String?.orDefault(default: String): String {
return this ?: default
}
result = nullName?.orDefault()
( bio: String?)
{
bioLength = profile?.bio?.length ?:
println()
}
{
value: String? =
{
value?.let { current ->
value = current.uppercase()
}
}
}
Safe call chains short-circuit at the first null, making deeply nested optional
access clean and safe without multiple null checks.
Elvis Operator and Null Coalescing
The Elvis operator ?: provides default values for null expressions, enabling
concise fallback logic without verbose if-else statements.
val name: String? = null
val displayName = name ?: "Guest"
println(displayName)
fun getUserCity(employee: Employee?): String {
return employee?.company?.address?.city ?: "Unknown"
}
fun calculateTotal(subtotal: Double?, taxRate: Double?): Double {
val sub = subtotal ?: 0.0
val tax = taxRate ?: 0.15
return sub * (1 + tax)
}
fun requireName(name: String?): String {
return name ?: throw IllegalArgumentException("Name required")
}
fun processUser(user: User?) {
val u = user ?: return
println("Processing ${u.name}")
}
fun findValidValue(
primary: String?,
secondary: String?,
tertiary: ?
): String {
primary ?: secondary ?: tertiary ?:
}
{
timeout: ? =
: {
timeout ?:
}
}
(name: String?) {
serviceName: String = name ?:
}
: String? =
: String? =
: String {
fetchFromCache() ?: fetchFromNetwork() ?:
}
The Elvis operator evaluates the right side only if the left side is null,
supporting lazy evaluation of default values.
Smart Casts
Smart casts automatically cast nullable types to non-nullable after null checks,
eliminating redundant casts and improving code clarity.
fun printLength(text: String?) {
if (text != null) {
println(text.length)
}
}
fun processName(name: String?): Int {
return if (name != null) {
name.length
} else {
0
}
}
fun requireUser(user: User?): User {
if (user == null) {
throw IllegalStateException("User required")
}
return user
}
fun getLength(text: String?): Int {
val nonNull = text ?: return 0
return nonNull.length
}
fun describe(obj: Any?): String {
return {
obj == ->
obj String ->
obj ->
->
}
}
{
value?.let { nonNull ->
println(nonNull.uppercase())
}
}
( value: String?) {
{
(value != ) {
}
localValue = value
(localValue != ) {
println(localValue.length)
}
}
}
{
require(value != )
println(value.length)
}
Smart casts work with immutable variables and val properties but not var
properties, which could change between the check and usage.
Not-Null Assertion and Platform Types
The not-null assertion operator !! explicitly throws NPE if a value is null,
useful for cases where null is impossible but the compiler cannot verify.
fun processName(name: String?) {
val length = name!!.length
println("Length: $length")
}
fun initializeFromConfig(config: Map<String, String>) {
val apiKey = config["api_key"]!!
val endpoint = config["endpoint"]!!
println("Configured with $apiKey at $endpoint")
}
val city = employee!!.company!!.address!!.city!!
val city2 = employee?.company?.address?.city
?: throw IllegalStateException("City required")
class JavaInterop {
fun useJavaApi() {
val javaString = JavaClass.getString()
val length: Int? = javaString?.length
val length2: Int = javaString!!.length
explicitString: String = JavaClass.getString()
}
}
: java.lang.String? {
}
{
apiClient: ApiClient
{
apiClient = client
}
{
(::apiClient.isInitialized) {
apiClient.request()
}
}
}
{
{}
}
: Data {
Json.parse(json!!)
}
Json {
: Data = Data()
}
( value: String = )
{
{
: String =
}
}
The !! operator should be used sparingly and only when you have high
confidence the value is non-null, as it reintroduces crash potential.
Nullability in Collections and Generics
Collections and generic types support nullability at both the container and
element levels, requiring clear distinction between nullable elements and
nullable collections.
val listWithNulls: List<String?> = listOf("A", null, "B")
val nullableList: List<String>? = null
fun filterNulls(items: List<String?>): List<String> {
return items.filterNotNull()
}
val filtered = filterNulls(listOf("A", null, "B"))
println(filtered)
val userIds: Map<String, Int?> = mapOf(
"alice" to 1,
"bob" to null,
"charlie" to 3
)
fun getUserId(name: String): Int? {
return userIds[name]
}
fun <T> firstOrNull(list: List<T>): T? {
return list.firstOrNull()
}
fun <T : Any> nonNullable(value: T) {
println(value.toString())
}
<>( value: T?)
stringContainer = Container<String>()
intContainer = Container<>()
: List<String> {
.filterNotNull().map(transform)
}
result = listOf(, , ).filterNotNullAndMap { it.toString() }
: {
== || .isEmpty()
}
empty: String? =
println(empty.isNullOrEmpty())
{
items
.filterNotNull()
.map { it.uppercase() }
.forEach { println(it) }
}
<> {
: T?
}
<> {
}
Understanding the distinction between List<String?> (list of nullable strings)
and List<String>? (nullable list) is crucial for correct null handling.
Designing Null-Safe APIs
Designing APIs with appropriate nullability improves usability and prevents
misuse by making null expectations explicit in the type system.
class UserRepository {
fun save(user: User) {
println("Saving ${user.name}")
}
}
class UserService {
fun findById(id: Int): User? {
return if (id > 0) User(id, "Alice") else null
}
fun getAllUsers(): List<User> {
return emptyList()
}
}
class QueryBuilder {
private var table: String? = null
private var where: String? = null
private var orderBy: String? = null
fun from(table: String) = apply { this.table = table }
fun where = apply { . = condition }
= apply { .orderBy = column }
: String {
t = table ?: IllegalStateException()
w = ?.let { } ?:
o = orderBy?.let { } ?:
.trim()
}
}
(
timeout: ? = ,
retries: ? = ,
debug: ? =
) {
= timeout ?:
= retries ?:
= debug ?:
}
<> {
<>( value: T) : Result<T>()
( message: String) : Result<>()
NotFound : Result<>()
}
: Result<User> {
{
id < -> Result.Error()
id == -> Result.NotFound
-> Result.Success(User(id, ))
}
}
{
: String? {
(email.contains()) {
} {
}
}
}
{
{
{
user = User(, )
onSuccess(user)
} (e: Exception) {
onError?.invoke(e.message ?: )
}
}
}
Good API design minimizes nullability where possible, using empty collections
instead of null lists and sealed classes for richer optional value semantics.
Best Practices
-
Prefer non-nullable types by default to maximize compile-time safety and
reduce null checks throughout the codebase
-
Use safe call operator ?. for chaining instead of multiple null checks
to keep code concise and readable
-
Provide defaults with Elvis operator ?: rather than verbose if-else
chains for simple fallback scenarios
-
Return empty collections instead of null to simplify client code and
eliminate null checks for collection results
-
Leverage smart casts after null checks to avoid redundant casts and let
the compiler track non-null guarantees
-
Minimize use of !! operator and document assumptions when used, as it
reintroduces crash potential
-
Design APIs with explicit nullability to communicate intent clearly and
prevent misuse of nullable values
-
Use lateinit for non-null deferred initialization instead of nullable
properties with manual null checks
-
Apply nullable extensions to provide utilities like isNullOrEmpty() for
cleaner null handling in common scenarios
-
Prefer sealed classes over nullable types for richer semantics when
representing success, error, and absent states
Common Pitfalls
-
Overusing nullable types when values should never be null makes APIs
harder to use and adds unnecessary checks
-
Chaining !! operators creates unclear crash points; use safe calls or
explicit validation instead
-
Ignoring platform types from Java can cause NPEs; treat Java return
values as nullable unless documented
-
Using var properties for smart casts fails because vars can change; use
local val copies for smart casting
-
Returning null collections instead of empty collections forces clients to
handle two separate cases unnecessarily
-
Not considering nullability in equals/hashCode can cause unexpected
behavior in collections and comparisons
-
Forgetting that safe calls return nullable types leads to unexpected null
values propagating through code
-
Using nullable primary constructor parameters without defaults makes
object creation unnecessarily complex
-
Creating deeply nested nullable structures becomes unwieldy; flatten or
use sealed classes for clarity
-
Not documenting null semantics in complex APIs leaves callers guessing
when nulls are valid or what they represent
When to Use This Skill
Use Kotlin null safety when building any Kotlin application to eliminate
NullPointerExceptions and make null handling explicit, including Android apps,
server-side services, and multiplatform projects.
Apply nullable types and safe call operators when working with data from
external sources like network APIs, databases, or user input where values may be
absent.
Employ Elvis operator and smart casts when handling optional configuration,
defaults, or fallback values to keep code concise and readable.
Leverage null-safe design patterns when building libraries or frameworks to
create APIs that are hard to misuse and clearly communicate expectations.
Use sealed classes and Result types for richer optional semantics in domain
models where null is insufficient to represent different states.
Resources