Before submitting a new issue
Bug summary
Summary
On Android we see Application Not Responding (ANR) with a deadlock between the React Native bridge thread (mqt_v_native) and the main thread, involving RNSpeechModule (com.mhpdev.speech.RNSpeechModule) and Android’s TextToSpeech.
What goes wrong
ensureInitialized runs operation() while still inside synchronized(initLock) when isInitialized is already true.
- Many operations (e.g.
stop) call synthesizer.isSpeaking / synthesizer.stop(), which contend with TextToSpeech’s internal synchronization.
processPendingOperations() is invoked from the TTS init success path on the main thread and starts with synchronized(initLock), while TextToSpeech may already hold locks from the init/dispatch flow.
Together this can yield:
| Thread |
Holds |
Waits for |
| main |
TextToSpeech lock (init callback path) |
initLock (processPendingOperations) |
| bridge |
initLock (ensureInitialized + isInitialized branch) |
TextToSpeech lock (isSpeaking / stop in operation()) |
That is a classic lock-order inversion between module initLock and the platform TTS lock.
Suggested fix
-
Do not call operation() while holding initLock.
Under synchronized(initLock) only: read state, enqueue to pendingOperations, or decide that init is required. After releasing the lock, if TTS was already initialized, invoke operation() (with try/catch and promise reject as today).
-
Optional hardening: after TextToSpeech.SUCCESS, drain pending operations with mainHandler.post { processPendingOperations() } so work runs after the framework unwinds the init callback, reducing sensitivity to future Android lock ordering.
Affected code (conceptual)
ensureInitialized — isInitialized -> { operation() } inside synchronized(initLock).
processPendingOperations — first line acquires initLock, may run from main-thread TTS init path.
Environment (fill in as needed)
- Library:
@mhpdev/react-native-speech (e.g. 1.4.x)
- React Native: 0.83.x (or your version)
- Android: API 35+ / “Android 16” (or your Sentry device profile)
Repro / notes
Hard to make 100% deterministic; stress concurrent calls during/just after first TTS use: Speech.stop(), Speech.initialize/configure, getAvailableVoices, speak, navigation-style rapid stop/speak patterns.
Library version
1.4.1
Environment info
System:
OS: macOS 26.2
CPU: (14) arm64 Apple M4 Pro
Memory: 120.02 MB / 24.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node: Not Found
Yarn:
version: 1.22.22
path: /opt/homebrew/bin/yarn
npm:
version: 10.9.2
path: /Users/remihindriks/Library/Application
Support/Herd/config/nvm/versions/node/v22.14.0/bin/npm
Watchman:
version: 2025.05.26.00
path: /opt/homebrew/bin/watchman
Managers:
CocoaPods:
version: 1.16.2
path: /Users/remihindriks/.rbenv/shims/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 25.2
- iOS 26.2
- macOS 26.2
- tvOS 26.2
- visionOS 26.2
- watchOS 26.2
Android SDK: Not Found
IDEs:
Android Studio: 2024.3 AI-243.25659.59.2432.13423653
Xcode:
version: 26.2/17C52
path: /usr/bin/xcodebuild
Languages:
Java:
version: 17.0.13
path: /usr/bin/javac
Ruby:
version: 3.2.2
path: /Users/remihindriks/.rbenv/shims/ruby
npmPackages:
"@react-native-community/cli":
installed: 20.0.0
wanted: 20.0.0
react:
installed: 19.2.0
wanted: 19.2.0
react-native:
installed: 0.83.2
wanted: 0.83.2
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: true
iOS:
hermesEnabled: true
newArchEnabled: true
Steps to reproduce
- …
- …
Reproducible example repository
bru
Before submitting a new issue
Bug summary
Summary
On Android we see Application Not Responding (ANR) with a deadlock between the React Native bridge thread (
mqt_v_native) and the main thread, involvingRNSpeechModule(com.mhpdev.speech.RNSpeechModule) and Android’sTextToSpeech.What goes wrong
ensureInitializedrunsoperation()while still insidesynchronized(initLock)whenisInitializedis alreadytrue.stop) callsynthesizer.isSpeaking/synthesizer.stop(), which contend withTextToSpeech’s internal synchronization.processPendingOperations()is invoked from the TTS init success path on the main thread and starts withsynchronized(initLock), whileTextToSpeechmay already hold locks from the init/dispatch flow.Together this can yield:
TextToSpeechlock (init callback path)initLock(processPendingOperations)initLock(ensureInitialized+isInitializedbranch)TextToSpeechlock (isSpeaking/stopinoperation())That is a classic lock-order inversion between module
initLockand the platform TTS lock.Suggested fix
Do not call
operation()while holdinginitLock.Under
synchronized(initLock)only: read state, enqueue topendingOperations, or decide that init is required. After releasing the lock, if TTS was already initialized, invokeoperation()(with try/catch and promise reject as today).Optional hardening: after
TextToSpeech.SUCCESS, drain pending operations withmainHandler.post { processPendingOperations() }so work runs after the framework unwinds the init callback, reducing sensitivity to future Android lock ordering.Affected code (conceptual)
ensureInitialized—isInitialized -> { operation() }insidesynchronized(initLock).processPendingOperations— first line acquiresinitLock, may run from main-thread TTS init path.Environment (fill in as needed)
@mhpdev/react-native-speech(e.g. 1.4.x)Repro / notes
Hard to make 100% deterministic; stress concurrent calls during/just after first TTS use:
Speech.stop(),Speech.initialize/configure,getAvailableVoices,speak, navigation-style rapid stop/speak patterns.Library version
1.4.1
Environment info
System: OS: macOS 26.2 CPU: (14) arm64 Apple M4 Pro Memory: 120.02 MB / 24.00 GB Shell: version: "5.9" path: /bin/zsh Binaries: Node: Not Found Yarn: version: 1.22.22 path: /opt/homebrew/bin/yarn npm: version: 10.9.2 path: /Users/remihindriks/Library/Application Support/Herd/config/nvm/versions/node/v22.14.0/bin/npm Watchman: version: 2025.05.26.00 path: /opt/homebrew/bin/watchman Managers: CocoaPods: version: 1.16.2 path: /Users/remihindriks/.rbenv/shims/pod SDKs: iOS SDK: Platforms: - DriverKit 25.2 - iOS 26.2 - macOS 26.2 - tvOS 26.2 - visionOS 26.2 - watchOS 26.2 Android SDK: Not Found IDEs: Android Studio: 2024.3 AI-243.25659.59.2432.13423653 Xcode: version: 26.2/17C52 path: /usr/bin/xcodebuild Languages: Java: version: 17.0.13 path: /usr/bin/javac Ruby: version: 3.2.2 path: /Users/remihindriks/.rbenv/shims/ruby npmPackages: "@react-native-community/cli": installed: 20.0.0 wanted: 20.0.0 react: installed: 19.2.0 wanted: 19.2.0 react-native: installed: 0.83.2 wanted: 0.83.2 react-native-macos: Not Found npmGlobalPackages: "*react-native*": Not Found Android: hermesEnabled: true newArchEnabled: true iOS: hermesEnabled: true newArchEnabled: trueSteps to reproduce
Reproducible example repository
bru