Yeda AI Tips · #117

Español

Reject Double Bang in Generated Kotlin

Every double bang in AI-generated Kotlin is a crash waiting for prod. The !! operator converts any value to a non-nullable type — and if the value is null at that moment, it throws a NullPointerException right there. Kotlin's docs list !! as one of only three ways an NPE can happen in pure Kotlin code. When an AI assistant sprinkles it through generated code, it's not asserting knowledge — it's silencing the compiler because that's the shortest token path to code that compiles.

Why generated code loves !!

Code generators optimize for "compiles on the first try." Faced with a nullable type, the two honest options — propagate the nullability or handle the null branch — both cost more code. !! costs two characters. The same shortcut appears as the initialization-phase nullable field: a var user: User? = null that "will definitely be set before anyone reads it." That promise lives in a comment or in the author's head, not in the type system, so every read site either handles a null that "can't happen" or double-bangs it. Either way, absence has been converted from a compile-time fact into a runtime bet.

Fix 1: make absence explicit and handle it

If a value can genuinely be missing, keep the type nullable and use the operators the language gives you:

// AI draft — deferred crash
val name = response.user!!.name

// Explicit — compiler-checked
val name = response.user?.name ?: "anonymous"

The safe call ?. returns null instead of throwing; the Elvis operator ?: supplies the fallback. Chains compose: bob?.department?.head?.name is null if any link is null — no branch of that expression can throw an NPE.

Fix 2: model closed states with sealed results

When "null" is standing in for a state — not loaded yet, loaded, failed — a nullable field is the wrong tool. A sealed hierarchy names each state, and because all direct subclasses of a sealed class are known at compile time, when over it is exhaustive with no else:

sealed interface LoadState {
    data object Loading : LoadState
    data class Ready(val user: User) : LoadState
    data class Failed(val cause: Exception) : LoadState
}

fun render(s: LoadState) = when (s) {
    LoadState.Loading   -> showSpinner()
    is LoadState.Ready  -> showUser(s.user)
    is LoadState.Failed -> showError(s.cause)
    // no else — the compiler proves every case is covered
}

Add a fourth state next month and every when that forgot it becomes a compile error, not a 3 a.m. page. That is the payoff: the compiler forces you to handle the empty case, and whole classes of crashes become unreachable.

Rules of thumb for review

You see in the diffAsk for instead
foo!!.barfoo?.bar + ?: fallback, or fix the type upstream
var x: T? = null set "later" during initConstructor injection, or a sealed Loading/Ready state
lateinit var read before a guaranteed assignment::x.isInitialized guard — or restructure so init is ordered
when on a sealed type with an else branchExhaustive when, no else, so new cases fail the build
Unannotated value from a Java APIExplicit Kotlin type on the public signature

Power-user notes

Resources

Building with AI-generated code? Yeda AI designs, audits, and ships production LLM systems.

Talk to us · Read the blog