Skip to content

Android ANR/deadlock: ensureInitialized holds initLock while calling TextToSpeech APIs #11

@RemiHin

Description

@RemiHin

Before submitting a new issue

  • I tested using the latest version of the library, as the bug might be already fixed.
  • I tested using a supported version of react native.
  • I checked for possible duplicate issues, with possible answers.

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

  1. ensureInitialized runs operation() while still inside synchronized(initLock) when isInitialized is already true.
  2. Many operations (e.g. stop) call synthesizer.isSpeaking / synthesizer.stop(), which contend with TextToSpeech’s internal synchronization.
  3. 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)

  • ensureInitializedisInitialized -> { 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions