A payment screen checks a nullable Compose state for null, then force-unwraps it on the next line. Under normal use it works. Under rapid interaction, a crash report shows a NullPointerException on the
!!. How can a value that was just checked be null?
var bankMismatchData by remember { mutableStateOf<MismatchData?>(null) }
if (showBankMismatchSheet && bankMismatchData != null) {
val mismatchData = bankMismatchData!!
BankMismatchSheet(mismatchData)
}
This Compose null check crash comes from reading the same mutable state twice. bankMismatchData is a local delegated property. Every read goes through the delegate’s getter. The getter reads the snapshot state. The null check is one read. The !! is a second read. Kotlin knows the two reads might differ. That’s why it refuses to smart-cast here, so you end up writing !!. Within one composition pass, both reads usually see the same value. But when a read happens inside a lambda that runs later, it can see a newer value. Sheet content and callbacks are common examples of such lambdas. If another part of the screen has set the state to null by then, the !! throws. The fix is to read the state once into a local val. Then check and use that local. The compiler can smart-cast it. Nothing can change it between the check and the use.
A Compose null check crash from a real app
This pattern appears in a May 2026 bug report on Mifos Pay, an open-source wallet app. The report covers FastMpayScreen. That screen handles QR-code payments across banks. Two nullable mutable states, bankMismatchData and pendingAmountConfirmation, were each checked for null and then force-unwrapped. The reporter describes a NullPointerException after scanning a cross-bank QR code and interacting rapidly while the sheet renders.
A contributor posted a fix from a fork. It reads each state into a local val before the null check. It also removes every !! from the file. When this was written, the upstream issue was still open.
Why Kotlin won’t smart-cast this state
Kotlin’s docs on smart cast prerequisites spell out the rule. Smart casts need a guarantee. The compiler must know the variable won’t change between the check and its use. Local vals always qualify, except local delegated properties. var properties never qualify.
var x by remember { mutableStateOf(...) } is a local delegated property. So the compiler won’t smart-cast it after a null check. That’s why the !! gets written in the first place. The compiler is telling you the two reads aren’t guaranteed to match. The !! gets past that compile error without fixing the cause.
The fix: read once, then branch
// Unsafe: two reads of a delegated state property
if (bankMismatchData != null) {
val mismatchData = bankMismatchData!!
BankMismatchSheet(mismatchData)
}
// Safe: one read, captured in a local val the compiler can smart-cast
val mismatchData = bankMismatchData
if (mismatchData != null) {
BankMismatchSheet(mismatchData)
}
The second version reads the state once. The local val can’t change, so the compiler smart-casts it to non-null inside the if. Any lambda inside BankMismatchSheet now captures the checked value, not the live state. This isn’t a Compose trick. It’s the same discipline Kotlin asks for with any mutable property.
What to look for in code review
Watch for remember { mutableStateOf(...) } with a nullable type, followed by a null check and a !!. The buggy version compiles cleanly and looks guarded. It may only fail under timing you rarely hit in manual testing. The same applies to var properties in ViewModels and other classes.
How to answer this in an interview
- Say that the check and the
!!are two separate reads of mutable state. - Explain why the compiler refused to smart-cast. The state is a local delegated property, so the compiler can’t guarantee both reads match.
- Name when the reads can differ. That happens when the second read runs later, inside a lambda such as sheet content or a callback.
- Give the fix as a rule. Read mutable state into a local
valonce, then branch on the local.
Common wrong answers:
- “The null check makes it safe.” It checks one read. The
!!is a different read. - “Wrap it in
try/catch.” That hides the crash and still renders with bad state. - “Recomposition is random.” The rule is specific. Reads inside lambdas can run later and see newer values.
Another Kotlin feature quietly changes what gets compared. See why a data class’s equals() ignores the parent class.
GrindLoop’s Bug-Squash track turns failure patterns like this one into live debugging drills, each with a reviewed fix.
Failed the interview? Not the next one.
4 thoughts on “The Null Check That Didn’t Save You”