Skip to main content

14.7.13 - Thread Safety & Error Handling

· 3 min read
Hyo
React Native IAP Maintainer

This release fixes two critical reliability issues: thread safety for purchase event listeners across both platforms, and unhandled JNI exceptions during Android initialization.

Thread Safety for Listener Operations (#3152, #3157)

Problem

When purchase events fired concurrently (e.g., during high-volume purchases or rapid listener add/remove), listener arrays could be modified while being iterated — causing ConcurrentModificationException on Android and potential crashes on iOS. This resulted in purchase events being silently lost.

Fix

Both platforms now use a synchronized + snapshot pattern: listener arrays are protected by a lock, and a snapshot copy is taken before iteration. This ensures safe concurrent access without blocking event delivery.

iOS

  • Added NSLock to protect all listener arrays (purchaseUpdatedListeners, purchaseErrorListeners, promotedProductListeners)
  • sendPurchaseUpdate and sendPurchaseError iterate over snapshot copies
  • Error dedup state and delivery tracking collections are also protected
  • cleanupExistingState clears all state atomically within the lock

Android

  • Applied snapshot pattern to userChoiceBilling and developerProvidedBilling listeners for consistency
  • Added synchronized for clearing these listener arrays in endConnection

Before & After

// Before: Concurrent modification could crash or silently drop events
listeners.forEach { it(event) } // ❌ Another thread modifies the list

// After: Snapshot ensures safe iteration
val snapshot = lock.withLock { ArrayList(listeners) }
snapshot.forEach { it(event) } // ✅ Isolated from modifications

Android JNI Exception Handling (#3158)

Problem

Some users reported a cryptic error during initConnection:

Unknown N8facebook3jni12JniExceptionE error.

This occurred because the openIap object is lazy-initialized, and its first access happens during setActivity() or listener registration — both of which were outside any try-catch block. On devices without Google Play Services or with billing client issues, the raw JNI exception propagated unhandled, producing the mangled C++ class name as the error message.

Fix

  • Wrapped setActivity and listener registration in try-catch blocks
  • Converts exceptions to structured OpenIapException with error code init-connection and descriptive messages
  • Added CancellationException passthrough to preserve coroutine cancellation semantics
  • On listener registration failure, initDeferred is completed exceptionally to prevent concurrent callers from deadlocking

Before & After

// Before: Cryptic error with no actionable information
catch (error) {
// error.message = "Unknown N8facebook3jni12JniExceptionE error."
// error.code = undefined ❌
}

// After: Structured error with clear context
catch (error) {
// error.code = "init-connection" ✅
// error.message = "Failed to register billing listeners: <details>" ✅
}

Installation

yarn add react-native-iap@14.7.13