Difficulty Level: Intermediate
What Youâll Learn
- Why the
!!
operator is dangerous and causes app crashes - 4 safe alternatives to the not-null assertion operator
- Real-world examples of proper null safety patterns
- Best practices for reliable Android development
- How to migrate existing
!!
usage to safer alternatives
Quick Answer: Replace
!!
with safe calls (?.
), Elvis operator (?:
), explicit null checks, orlet
functions to prevent crashes and write reliable Kotlin code.
Kotlinâs !!
operator (not-null assertion) is one of the most misused features by developers new to the language. While it seems like a quick fix for null safety warnings, itâs actually a major source of production crashes.
The !!
operator forcefully converts a nullable type to non-nullable, throwing a KotlinNullPointerException if the value is null. This defeats the entire purpose of Kotlinâs null safety systemâturning compile-time safety into runtime crashes.
Common scenario: Developers coming from Java or other languages often use !!
to silence compiler warnings, especially when dealing with API responses, database queries, or intent extras. This creates brittle code that works during testing but crashes in production when unexpected nulls appear.
Whether youâre coming from Java, transitioning from Unity C# development to native Android, or new to programming, this guide explains why !!
is dangerous and demonstrates safe alternatives for reliable code. These principles are essential when building modern Jetpack Compose applications where null state can break your UI.
The Kotlin Null Safety Problem with !!
Real-world example: Apps commonly crash when using
!!
on API responses during network issues, database queries that return null, or missing intent extras.
Kotlinâs compiler enforces strict null safety checks that can frustrate developers accustomed to less restrictive languages. The temptation to bypass these checks with !!
is strong but dangerous:
// Bad: Using !! everywhere to silence compiler warnings
class UserProfileActivity : AppCompatActivity() {
private lateinit var nameEditText: EditText
private lateinit var emailEditText: EditText
private fun saveUserProfile() {
val name = nameEditText.text.toString()
val email = emailEditText.text.toString()
// Dangerous: What if the API returns null?
val user = getUserFromApi()!!
user.updateProfile(name, email)
// Dangerous: What if the database query fails?
val preferences = getSharedPreferences("user_prefs", MODE_PRIVATE)!!
preferences.edit()!!.putString("last_saved", getCurrentTimestamp()!!)!!.apply()
// Dangerous: What if the intent has no extras?
val userId = intent.extras!!.getString("user_id")!!
processUser(userId)
}
}
Critical issue: This code compiles and may run successfully during testing, but each !!
creates a potential crash point. Every !!
operator is an assertion that the value will never be nullâan assumption that often proves wrong in production.
Production Risk: Each
!!
operator represents a potential crash point that may only surface under specific conditions, making debugging extremely difficult.
Why !! Is Dangerous
1. Trading Compile-Time Safety for Runtime Crashes
Kotlinâs null safety provides compile-time guarantees that prevent runtime errors. Using !!
bypasses these protections, converting safe code into crash-prone implementations.
2. Poor Error Messages
!!
crashes produce generic KotlinNullPointerException messages without context about which specific value was null, making debugging difficult.
3. False Confidence
!!
creates an illusion of certainty about null safety while actually introducing significant crash risks.
4. Production Debugging Challenges
!!
crashes often occur only under specific production conditions that are difficult to reproduce in development, making debugging time-consuming and complex.
Key principle: Before using !!
, always ask âWhat happens if this IS null?â If the answer is âIt canât be nullâ, thatâs exactly when it will be.
Safe Kotlin Null Safety Alternatives to !!
These alternatives arenât just saferâtheyâre more expressive and lead to cleaner, more maintainable code:
Alternative 1: Safe Call Operator (?.)
The safe call operator ?.
executes the operation only if the value is not null. Itâs polite, itâs safe, and it wonât crash your app:
// Good: Using safe calls
class UserProfileActivity : AppCompatActivity() {
private lateinit var nameEditText: EditText
private lateinit var emailEditText: EditText
private fun saveUserProfile() {
val name = nameEditText.text.toString()
val email = emailEditText.text.toString()
// Safe: Only calls updateProfile if user is not null
getUserFromApi()?.updateProfile(name, email)
// Safe: Only saves preferences if available
getSharedPreferences("user_prefs", MODE_PRIVATE)
?.edit()
?.putString("last_saved", getCurrentTimestamp())
?.apply()
}
}
Benefits:
- No crashes - Operations are skipped if the value is null
- Readable code - Intent is clear and explicit
- Idiomatic Kotlin - Uses language features as designed
- Chainable - Multiple safe calls can be combined
Alternative 2: Elvis Operator (?:) for Default Values
Use the Elvis operator to provide fallback values:
// Good: Providing sensible defaults
class UserRepository {
private fun processUserData(userData: UserData?) {
// Provide default values instead of crashing
val userName = userData?.name ?: "Anonymous User"
val userAge = userData?.age ?: 0
val isVerified = userData?.isEmailVerified ?: false
// Safe to use these values
updateUserInterface(userName, userAge, isVerified)
}
private fun loadUserPreferences(): UserPreferences {
val savedPrefs = getSharedPreferences("user_prefs", MODE_PRIVATE)
?.getString("preferences", null)
// Return default preferences if saved ones don't exist
return if (savedPrefs != null) {
parseUserPreferences(savedPrefs)
} else {
UserPreferences.createDefault()
}
}
}
Alternative 3: Explicit Null Checks with Early Returns
Sometimes you need to handle null cases explicitly:
// Good: Explicit null handling with early returns
class OrderProcessor {
private fun processOrder(orderId: String?) {
// Handle null case explicitly
if (orderId.isNullOrEmpty()) {
showError("Invalid order ID")
return
}
val order = orderRepository.findById(orderId)
if (order == null) {
showError("Order not found")
return
}
// Safe to process order here
calculateTotal(order)
sendConfirmationEmail(order.customerEmail)
}
private fun updateUserStatus(user: User?) {
user ?: run {
Log.w("UserManager", "Attempted to update null user")
return
}
// User is guaranteed to be non-null here
user.lastActiveTime = System.currentTimeMillis()
userRepository.save(user)
}
}
Alternative 4: let Function for Safe Operations
Use the let
function to perform operations only on non-null values:
// Good: Using let for safe operations
class ImageLoader {
private fun loadUserAvatar(user: User?) {
// Only execute the block if user is not null
user?.profileImageUrl?.let { imageUrl ->
if (imageUrl.isNotEmpty()) {
loadImageIntoView(imageUrl, avatarImageView)
}
}
}
private fun processApiResponse(response: ApiResponse?) {
response?.data?.let { data ->
when (data.status) {
"success" -> handleSuccess(data.result)
"error" -> handleError(data.errorMessage)
else -> handleUnknownStatus()
}
} ?: run {
// Handle the case where response or data is null
handleNetworkError()
}
}
}
When !! Might Be Acceptable
There are very rare cases where !!
is acceptable, but they should be heavily documented:
// Acceptable: When you have a guarantee from the platform
class MainActivity : AppCompatActivity() {
private fun handleIntent() {
// Platform guarantee: Activities always have an intent
val action = intent.action
// Still prefer safe alternatives when possible
val extras = intent.extras
if (extras != null) {
val userId = extras.getString("user_id")
// Handle userId safely
}
}
}
// Acceptable: After explicit null check (but prefer smart casting)
private fun processUser(user: User?) {
if (user != null) {
// Kotlin smart cast makes !! unnecessary here anyway
user.updateLastSeen() // No !! needed, user is smart cast to non-null
}
}
Safe Null Handling Patterns
Pattern 1: Builder Pattern with Validation
// Good: Safe builder pattern
class UserRequestBuilder {
private var name: String? = null
private var email: String? = null
private var age: Int? = null
fun setName(name: String?): UserRequestBuilder {
this.name = name?.trim()?.takeIf { it.isNotEmpty() }
return this
}
fun setEmail(email: String?): UserRequestBuilder {
this.email = email?.lowercase()?.takeIf { it.contains("@") }
return this
}
fun build(): UserRequest? {
val validName = name ?: return null
val validEmail = email ?: return null
return UserRequest(
name = validName,
email = validEmail,
age = age ?: 0
)
}
}
Pattern 2: Extension Functions for Safe Operations
// Good: Extension functions for common null-safe operations
fun String?.isValidEmail(): Boolean {
return this?.contains("@") == true && this.contains(".")
}
fun List<Any>?.isNotNullOrEmpty(): Boolean {
return this != null && this.isNotEmpty()
}
fun <T> T?.orDefault(default: T): T {
return this ?: default
}
// Usage
private fun validateUserInput() {
val emailInput = emailEditText.text.toString()
if (!emailInput.isValidEmail()) {
showError("Please enter a valid email")
return
}
val userTags = getUserTags().orDefault(emptyList())
if (userTags.isNotNullOrEmpty()) {
processUserTags(userTags)
}
}
Migration Strategy: Fixing Existing !! Usage
If you have code with !!
that you need to fix:
Step 1: Identify the Risk Level
// High risk: External data sources
val userData = apiClient.getUser()!! // High crash risk
// Medium risk: Internal nullability
val preferences = getSharedPreferences("app", MODE_PRIVATE)!! // Medium risk
// Low risk: Platform guarantees (but still avoid)
val context = getApplicationContext()!! // Usually safe but unnecessary
Step 2: Replace with Safe Alternatives
// Fixed: Safe handling of external data
val userData = apiClient.getUser()
if (userData != null) {
processUser(userData)
} else {
handleUserNotFound()
}
// Fixed: Safe preferences access
getSharedPreferences("app", MODE_PRIVATE)
?.getString("user_id", null)
?.let { userId ->
loadUserProfile(userId)
}
Frequently Asked Questions
Q: When is it actually safe to use !!
?
A: Only when you have a platform guarantee (like intent
in an Activity) or after an explicit null check where Kotlinâs smart casting would work anyway. Always document why itâs safe.
Q: Whatâs the performance difference between ?.
and !!
?
A: Negligible. The safe call operator adds minimal overhead compared to the cost of handling crashes and debugging null pointer exceptions.
Q: Should I use ?.
or explicit null checks?
A: Use ?.
for simple operations and explicit checks when you need different behavior for null vs non-null cases. Explicit checks are better for complex conditional logic.
Q: How do I convert existing !!
code safely?
A: 1) Identify the risk level, 2) Replace with appropriate safe alternatives, 3) Add unit tests with null inputs, 4) Test thoroughly in different scenarios.
Q: Does null safety affect app performance?
A: Null safety improves performance by preventing crashes and reducing debugging time. The runtime overhead is minimal compared to the benefits.
Best Practices Summary
- Treat !! as a code smell: Every
!!
should be justified with a comment explaining why itâs safe - Use safe calls (
?.
) by default: Let the operation gracefully handle null cases - Provide defaults with Elvis (
?:
): Give meaningful fallback values - Handle null cases explicitly: Use early returns or explicit null checks
- Use
let
,run
,also
: Use scope functions for safe operations - Create extension functions: Build reusable null-safe utilities
- Test null scenarios: Write unit tests that pass null values to your functions
Conclusion
Kotlinâs null safety system is an effective feature that prevents countless crashes when used properly. The !!
operator should be treated as a last resort, not a default solution.
Migration strategy:
- Start by replacing obvious
!!
usage with safe alternatives - Add unit tests that include null scenarios
- Gradually refactor remaining instances
- Document any remaining
!!
usage with clear justification
These null safety principles become especially important when building Compose applications where null state can cause UI issues. The same careful approach applies to mastering @Composable annotations and proper Compose state management patterns.
Best practice: When reaching for !!
, pause and ask âIs there a safer way?â In most cases, there is. For the rare exceptions, document why itâs safe to use.
Need help building reliable Android applications? Contact Angry Shark Studio for expert Kotlin and Android development services, or explore our mobile development portfolio to see how weâve built crash-free apps for clients worldwide.
Related Reading:

About Angry Shark Studio
Angry Shark Studio is a professional Unity AR/VR development studio specializing in mobile multiplatform applications and AI solutions. Our team includes Unity Certified Expert Programmers with extensive experience in AR/VR development.
Related Articles
More Articles
Explore more insights on Unity AR/VR development, mobile apps, and emerging technologies.
View All Articles