Skip to content

deleteMMKV() crashes on next foreground: AppState listener still calls checkContentChanged() on the destroyed instance #1082

Description

@robertovieira

What happens

Calling deleteMMKV(id) for an instance created in the same JS runtime crashes the app the
next time it returns to the foreground:

Exception Type:  EXC_CRASH (SIGABRT)
Termination:     SIGNAL 6 Abort trap: 6
Crashed Thread:  2  com.facebook.react.runtime.JavaScript

  0  libsystem_kernel.dylib   __pthread_kill
  1  libsystem_pthread.dylib  pthread_kill
  2  libsystem_c.dylib        abort
  3  libsystem_c.dylib        __assert_rtn
  4  mmkvdeletecrashrepro     mmkv::ScopedLock<mmkv::ThreadLock>::ScopedLock(mmkv::ThreadLock*)
  5  mmkvdeletecrashrepro     mmkv::ScopedLock<mmkv::ThreadLock>::ScopedLock(mmkv::ThreadLock*)
  6  mmkvdeletecrashrepro     mmkv::MMKV::checkContentChanged()
  7  mmkvdeletecrashrepro     margelo::nitro::mmkv::HybridMMKV::checkContentChanged()
  8  mmkvdeletecrashrepro     margelo::nitro::HybridFunction::callMethod<HybridMMKVSpec, void>(...)
     ... nitro HybridFunction / JSI / Hermes frames ...
 31  React                    facebook::jsi::Function::callWithThis(...)
 32  React                    facebook::react::ReactInstance::callFunctionOnModule(...)

In a debug build the assertion is Assertion failed: (m_lock), function ScopedLock, file ScopedLock.hpp, line 45. Full symbolicated stack is in the repro repo.

Reproducible example

https://github.com/robertovieira/mmkv-delete-crash-repro

Steps:

  1. Tap 1. create user-A and user-B
  2. Tap 2. deleteMMKV('user-B')
  3. Send the app to the background and bring it back
  4. Crash

Control test: tapping only button 1 and then backgrounding does not crash. The deleteMMKV()
in step 2 is what breaks it.

Native MMKV log right after step 2, showing the instance being destructed:

<MMKV_IO.cpp:1713::removeStorage> remove storage [user-B]
<MMKV.cpp:369::close> close [user-B]
<MMKV.cpp:340::clearMemoryCache> clearMemoryCache [user-B]
<MMKV.cpp:154::~MMKV> destruct [user-B]

Why it happens

createMMKV() registers a foreground listener for every instance
(src/addContentChangedListener/addContentChangedListener.ts):

const weakMmkv = new WeakRef(mmkv)
const listener = AppState.addEventListener('change', (state) => {
  if (state === 'active') weakMmkv.deref()?.checkContentChanged()
})
const finalization = new FinalizationRegistry((l) => l.remove())
finalization.register(mmkv, listener)

The WeakRef only guards against a garbage-collected wrapper. deleteMMKV() destroys the
native object while the JS wrapper is still alive and strongly reachable, so deref() returns
it and checkContentChanged() runs on a destructed instance whose m_lock is gone. The
FinalizationRegistry only fires after collection, which may never happen in time.

The repro keeps both instances in a module-level Map, which is what a real multi-account app
does. That is what makes the crash deterministic instead of dependent on GC timing.

Real-world trigger

A multi-account app that wipes the previous account's storage when another account signs in.
The OAuth flow itself backgrounds and foregrounds the app, so the crash lands a few seconds
after a successful login, with nothing in the Metro logs.

Suggested fix

Remove or invalidate the AppState listener inside deleteMMKV(), either by keeping a
Map<id, listener> alongside the instances, or by marking the HybridObject invalid on the
native side so checkContentChanged() becomes a no-op instead of asserting.

Workaround

Never call deleteMMKV() for an id already created in the current process. Call clearAll()
on the instance instead and defer the real deleteMMKV() to the next app launch, before any
createMMKV() for that id runs.

Environment

react-native-mmkv 4.3.2
react-native-nitro-modules 0.36.1
React Native 0.85.3 (new architecture)
Expo 56
MMKV core v2.4.0
Reproduced on iPhone 17 simulator, iOS 26.5, debug build

Also seen in a real app on a physical device (iPhone 15 Pro, iOS 26), same stack.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions