{"record":{"id":"68816299bd02b403","repo":"necolas/react-native-web","slug":"cannot-record-touch-end-without-a-touch-start-tou","errorCode":null,"errorMessage":"Cannot record touch end without a touch start.\nTouch End: ${printTouch(touch)}\nTouch Bank: ${printTouchBank(touchHistory)}","messagePattern":"Cannot record touch end without a touch start\\.\nTouch End: (.+?)\nTouch Bank: (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/react-native-web/src/modules/useResponderEvents/ResponderTouchHistoryStore.js","lineNumber":140,"sourceCode":"      `Touch Move: ${printTouch(touch)}\\n`,\n      `Touch Bank: ${printTouchBank(touchHistory)}`\n    );\n  }\n}\n\nfunction recordTouchEnd(touch: Touch, touchHistory): void {\n  const touchRecord = touchHistory.touchBank[getTouchIdentifier(touch)];\n  if (touchRecord) {\n    touchRecord.touchActive = false;\n    touchRecord.previousPageX = touchRecord.currentPageX;\n    touchRecord.previousPageY = touchRecord.currentPageY;\n    touchRecord.previousTimeStamp = touchRecord.currentTimeStamp;\n    touchRecord.currentPageX = touch.pageX;\n    touchRecord.currentPageY = touch.pageY;\n    touchRecord.currentTimeStamp = timestampForTouch(touch);\n    touchHistory.mostRecentTimeStamp = timestampForTouch(touch);\n  } else {\n    console.warn(\n      'Cannot record touch end without a touch start.\\n',\n      `Touch End: ${printTouch(touch)}\\n`,\n      `Touch Bank: ${printTouchBank(touchHistory)}`\n    );\n  }\n}\n\nfunction printTouch(touch: Touch): string {\n  return JSON.stringify({\n    identifier: touch.identifier,\n    pageX: touch.pageX,\n    pageY: touch.pageY,\n    timestamp: timestampForTouch(touch)\n  });\n}\n\nfunction printTouchBank(touchHistory): string {\n  const { touchBank } = touchHistory;","sourceCodeStart":122,"sourceCodeEnd":158,"githubUrl":"https://github.com/necolas/react-native-web/blob/a9de220ba9e65bdea540fb5322ffb1da2b0bf442/packages/react-native-web/src/modules/useResponderEvents/ResponderTouchHistoryStore.js#L122-L158","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Dispatch a matching touchstart (same identifier) before touchend in any synthetic event code","Handle touchcancel: if the platform sends touchcancel instead of touchend, don't forward a touchend to responder events afterward","Ensure listener/mount lifecycle spans the entire gesture — don't unmount or re-key the responder element mid-touch","Clear or reset ResponderTouchHistoryStore state between test runs so stale/cancelled touches don't corrupt subsequent assertions"],"exampleFix":"// before\ndispatchTouchEvent('touchend', { identifier: 1 });\n// after\ndispatchTouchEvent('touchstart', { identifier: 1, pageX: 10, pageY: 20 });\ndispatchTouchEvent('touchend', { identifier: 1, pageX: 10, pageY: 20 });","handlingStrategy":"validation","validationCode":"function hasTouchStart(touchHistory, identifier) {\n  return touchHistory != null && touchHistory.touchBank != null && touchHistory.touchBank[identifier] != null;\n}\nif (!hasTouchStart(touchHistory, touch.identifier)) return; // skip end\nrecordTouchEnd(touch, touchHistory);","typeGuard":"function isTrackedTouch(touchHistory, touch) {\n  return Boolean(touch && typeof touch.identifier === 'number' && touchHistory?.touchBank?.[touch.identifier]);\n}","tryCatchPattern":null,"preventionTips":["Pair every touchend with a preceding touchstart of the same identifier in synthetic event code","Treat touchcancel as terminal; do not emit touchend after a cancel for the same touch","Keep the responder element mounted for the full gesture duration; avoid re-keying mid-touch","Clear touch history state between automated test runs"],"tags":["touch","responder-events","event-ordering"],"backgroundTag":"touchend-without-touchstart","analyzedSha":"a9de220ba9e65bdea540fb5322ffb1da2b0bf442","analyzedAt":"2026-09-01T10:01:09.883Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}