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

Author: robertovieiraCreated Aug 2, 2026Updated Aug 12, 2026

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):

typescript
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.

Source: mrousavy/react-native-mmkv