Skip to content

Fix fatal crash Cannot read property 'translationX' of undefined when a touch event is serialized without allTouches#4316

Open
huextrat wants to merge 2 commits into
software-mansion:mainfrom
huextrat:fix/fix-touch-event-missing-alltouches-crash
Open

Fix fatal crash Cannot read property 'translationX' of undefined when a touch event is serialized without allTouches#4316
huextrat wants to merge 2 commits into
software-mansion:mainfrom
huextrat:fix/fix-touch-event-missing-alltouches-crash

Conversation

@huextrat

Copy link
Copy Markdown
Contributor

Note

This PR was written with AI assistance (Claude), based on a production crash investigated from Sentry.

Description

Fixes a fatal, unhandled production crash in apps using the v3 API (usePanGesture + GestureDetector) with Reanimated on Android (Fabric / New Architecture):

TypeError: Cannot read property 'translationX' of undefined
    at diffCalculator
    at getChangeEventCalculator
    at handleUpdateEvent
    at eventHandler

Since the error is thrown inside a worklet running on the UI thread, it results in a native CppException caught by the UncaughtExceptionHandler — a hard crash, not a JS redbox.

Observed trigger: very fast repeated taps on a screen with a GestureDetector + pan gesture (reproduced in production on a low-end Android 15 device, but the underlying race is not device-specific).

Root cause

The failure is a chain across the Android event serialization and the v3 event classification:

  1. Kotlin — GestureHandler.kt: dispatchTouchEvent() only guards on changedTouchesPayload != null. In dispatchTouchUpEvent(), extractAllPointersData() runs before the changed pointer is re-added to trackedPointers. If the tracked pointers were already cleared at that point (e.g. cancelPointers() fired by a rapid succession of taps racing with the touch-up dispatch), allTouchesPayload is null while changedTouchesPayload is still populated — so the event is dispatched anyway.

  2. Kotlin — RNGestureHandlerTouchEvent.kt: the serializer omitted the key entirely when the payload was null:

    handler.consumeAllTouchesPayload()?.let { putArray("allTouches", it) }

    → a touch event can reach JS without the allTouches key. Note that iOS always serializes allTouches/changedTouches as (possibly empty) arrays (RNGestureHandlerManager.mm initializes .allTouches = {}), so this was also a platform inconsistency.

  3. TS — src/v3/hooks/utils/eventUtils.ts: touch events are discriminated by key presence:

    export function isTouchEvent(...) { 'worklet'; return 'allTouches' in event; }

    Key absent → the touch event is misclassified as an update event.

  4. TS — src/v3/hooks/callbacks/eventHandler.ts: the event is routed to handleUpdateEvent()getChangeEventCalculator() reads current.handlerData (undefined for a touch event) and passes it to the gesture's diffCalculator, which reads current.translationXTypeError in a UI-thread worklet → fatal crash.

Fix

JS (defensive, worklet-safe, no behavior change for valid events):

  • eventHandler() now drops events that are neither state-change events, nor touch events, nor carry handlerData, instead of treating them as update events. (handlerData is present on all well-formed update events on every platform: Android createNativeEventData, web GestureHandler.ts, and jestUtils.)
  • getChangeEventCalculator() returns the event unchanged when handlerData is undefined instead of calling the diff calculator with undefined data.

Android (root cause):

  • RNGestureHandlerTouchEvent.createEventData() always serializes allTouches and changedTouches, falling back to empty arrays instead of omitting the keys — matching the iOS implementation.

Test plan

  • New regression test in src/__tests__/api_v3.test.tsx: fires a malformed touch event (no allTouches, no oldState, no handlerData) through a pan gesture's jsEventHandler. Without the JS fix it reproduces the exact production error (Cannot read properties of undefined (reading 'translationX') through diffCalculator); with the fix the event is dropped without invoking any callback, and subsequent valid update events still compute changeX/changeY correctly.
  • New unit tests for getChangeEventCalculator in src/__tests__/utils.test.tsx: change payload computed for well-formed events; event returned untouched when handlerData is missing.
  • Full Jest suite passes (80/80), yarn ts-check clean, yarn lint:js clean.
  • Android: spotlessCheck passes, library compiles via apps/basic-example (:react-native-gesture-handler:compileDebugKotlin — BUILD SUCCESSFUL).

huextrat added 2 commits July 15, 2026 15:31
On Android, a touch event can be serialized without the `allTouches` key
when its payload is lost in a race (e.g. rapid taps cancelling the
gesture while a touch-up is being dispatched). `isTouchEvent` relies on
`'allTouches' in event`, so such an event was misclassified as an update
event and passed to the change event calculator, which crashed reading
gesture-specific data (`Cannot read property 'translationX' of
undefined`) inside a UI-thread worklet - a fatal error in production.

- `eventHandler` now drops events that are neither state-change nor
  touch events and carry no `handlerData` instead of treating them as
  update events.
- `getChangeEventCalculator` returns the event unchanged when
  `handlerData` is missing instead of calling the diff calculator with
  undefined.

Valid events are unaffected; both guards are worklet-safe.
Copilot AI review requested due to automatic review settings July 15, 2026 13:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a hard crash in the v3 gesture event pipeline caused by a malformed Android touch event being serialized without the allTouches key, which led to misclassification as an update event and a UI-thread worklet exception.

Changes:

  • Make the v3 JS event router defensively drop events that are neither state-change nor touch events and that also lack handlerData, preventing the diff calculator from being called with undefined.
  • Make the v3 change calculator return the event unchanged when handlerData is missing (worklet-safe guard).
  • Fix Android touch event serialization to always include changedTouches and allTouches keys (empty arrays when payloads are unavailable), matching iOS behavior; add Jest regression/unit tests.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/react-native-gesture-handler/src/v3/hooks/utils/eventUtils.ts Adds a worklet-safe guard in getChangeEventCalculator and relies on allTouches key presence for touch-event discrimination.
packages/react-native-gesture-handler/src/v3/hooks/callbacks/eventHandler.ts Drops malformed non-touch/non-state-change events with missing handlerData instead of routing them to update handling.
packages/react-native-gesture-handler/src/tests/utils.test.tsx Adds unit tests covering normal change-payload computation and the missing-handlerData regression case.
packages/react-native-gesture-handler/src/tests/api_v3.test.tsx Adds an integration-style regression test ensuring malformed touch events don’t crash and don’t break subsequent valid updates.
packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/events/RNGestureHandlerTouchEvent.kt Ensures changedTouches and allTouches are always serialized (empty arrays as fallback) to avoid JS-side misclassification.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants