necolas/react-native-web · warning

Cannot record touch end without a touch start. Touch End: ${

Error message

Cannot record touch end without a touch start.
Touch End: ${printTouch(touch)}
Touch Bank: ${printTouchBank(touchHistory)}

What it means

This warning is emitted by ResponderTouchHistoryStore when recordTouchEnd receives a touch whose identifier is not in the touch bank — a 'touchend' arrives for a touch that never had a recorded 'touchstart'. The store warns via console.warn and skips updating history, so the dangling touch never completes a gesture. Like error 10, it signals inconsistent touch event ordering rather than a crash.

Source

Thrown at packages/react-native-web/src/modules/useResponderEvents/ResponderTouchHistoryStore.js:140

      `Touch Move: ${printTouch(touch)}\n`,
      `Touch Bank: ${printTouchBank(touchHistory)}`
    );
  }
}

function recordTouchEnd(touch: Touch, touchHistory): void {
  const touchRecord = touchHistory.touchBank[getTouchIdentifier(touch)];
  if (touchRecord) {
    touchRecord.touchActive = false;
    touchRecord.previousPageX = touchRecord.currentPageX;
    touchRecord.previousPageY = touchRecord.currentPageY;
    touchRecord.previousTimeStamp = touchRecord.currentTimeStamp;
    touchRecord.currentPageX = touch.pageX;
    touchRecord.currentPageY = touch.pageY;
    touchRecord.currentTimeStamp = timestampForTouch(touch);
    touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
  } else {
    console.warn(
      'Cannot record touch end without a touch start.\n',
      `Touch End: ${printTouch(touch)}\n`,
      `Touch Bank: ${printTouchBank(touchHistory)}`
    );
  }
}

function printTouch(touch: Touch): string {
  return JSON.stringify({
    identifier: touch.identifier,
    pageX: touch.pageX,
    pageY: touch.pageY,
    timestamp: timestampForTouch(touch)
  });
}

function printTouchBank(touchHistory): string {
  const { touchBank } = touchHistory;

View on GitHub (pinned to a9de220ba9)

Solutions

  1. Dispatch a matching touchstart (same identifier) before touchend in any synthetic event code
  2. Handle touchcancel: if the platform sends touchcancel instead of touchend, don't forward a touchend to responder events afterward
  3. Ensure listener/mount lifecycle spans the entire gesture — don't unmount or re-key the responder element mid-touch
  4. Clear or reset ResponderTouchHistoryStore state between test runs so stale/cancelled touches don't corrupt subsequent assertions

Example fix

// before
dispatchTouchEvent('touchend', { identifier: 1 });
// after
dispatchTouchEvent('touchstart', { identifier: 1, pageX: 10, pageY: 20 });
dispatchTouchEvent('touchend', { identifier: 1, pageX: 10, pageY: 20 });
Defensive patterns

Strategy: validation

Validate before calling

function hasTouchStart(touchHistory, identifier) {
  return touchHistory != null && touchHistory.touchBank != null && touchHistory.touchBank[identifier] != null;
}
if (!hasTouchStart(touchHistory, touch.identifier)) return; // skip end
recordTouchEnd(touch, touchHistory);

Type guard

function isTrackedTouch(touchHistory, touch) {
  return Boolean(touch && typeof touch.identifier === 'number' && touchHistory?.touchBank?.[touch.identifier]);
}

Prevention

When it happens

Trigger: A touchend/touchchange event whose touch.identifier has no entry in touchHistory.touchBank; caused by missing or swallowed touchstart, listener attachment after the gesture began, or synthetic event dispatch that ends a touch it never started.

Common situations: Test automation firing touchend without a preceding touchstart; fast taps where the browser coalesces or cancels events (touchcancel not handled by the test harness); page navigation/hydration occurring between touchstart and touchend; custom gesture libraries replaying only end events.

Related errors


AI-assisted analysis of necolas/react-native-web@a9de220ba9 (2026-09-01). Data as JSON: /api/errors/68816299bd02b403. Report an issue: GitHub.