How to End a Puppeteer Touch Gesture
End a Puppeteer touch with the TouchHandle returned by touchStart(). Learn when to use touchEnd() or tap(), and how to handle common touch errors.
To end a touch started with Puppeteer, save the TouchHandle returned by page.touchscreen.touchStart(x, y) and call await touch.end(). That dispatches touchend for that specific touch.
const touch = await page.touchscreen.touchStart(120, 240);
try {
await touch.move(180, 240); // Optional; omit for a stationary press.
} finally {
await touch.end();
}
Use page.touchscreen.tap(x, y) for a simple tap: Puppeteer handles both the start and end. Use the handle when you need to keep a touch active, move it, and release that particular touch yourself.
1. Complete runnable example
This JavaScript example starts Chromium, opens a page, dispatches a touch, and closes the browser. Puppeteer touch input requires a mobile or touch-enabled page configuration; enable touch emulation in the launch options. The handle’s end() call is placed in finally so the code attempts to release the touch if the optional movement or intervening work throws.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({
width: 390,
height: 844,
isMobile: true,
hasTouch: true,
});
await page.goto('https://example.com');
const touch = await page.touchscreen.touchStart(120, 240);
try {
await touch.move(180, 240); // Optional. Remove to keep the touch stationary.
// Perform any actions that must happen while the touch is held here.
} finally {
await touch.end();
}
} finally {
await browser.close();
}
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
The example uses CommonJS. In an ES module, replace const puppeteer = require('puppeteer') with import puppeteer from 'puppeteer'. Install Puppeteer in your project with npm install puppeteer. Coordinates are viewport coordinates in CSS pixels; choose a point inside the page content you intend to touch.
2. Choose the right touchscreen method
| Goal | Method | Behavior |
|---|---|---|
| Tap once | await page.touchscreen.tap(x, y) |
Dispatches the touch start and end sequence for you. |
| Hold, optionally move, then release that touch | const touch = await page.touchscreen.touchStart(x, y), then await touch.end() |
The handle identifies the touch that was started; touch.move(x, y) moves that touch. |
| End the first currently active touch | await page.touchscreen.touchEnd() |
Dispatches touchend on the first active touch. |
| Move the first currently active touch | await page.touchscreen.touchMove(x, y) |
Targets the first active touch, rather than using a specific handle. |
For code that owns a handle from touchStart(), prefer touch.end() because the target touch is explicit. The touchscreen-level touchEnd() is useful when first-active-touch behavior is what you want. Avoid mixing the two ending styles casually: once one method ends the active touch, a subsequent attempt to end the same touch may fail.
3. TypeScript pattern
The API behavior is the same in TypeScript. Let Puppeteer infer the handle type from touchStart(); this avoids depending on a type import that may vary across Puppeteer versions.
const touch = await page.touchscreen.touchStart(120, 240);
try {
await touch.move(180, 240);
} finally {
await touch.end();
}
Place this inside a function where page is your Puppeteer Page. If you only need a tap, use await page.touchscreen.tap(120, 240).
4. Important lifecycle details
- Keep the returned handle.
touchStart()resolves to a handle for the touch it started. Callend()on that handle to end that touch. - End once. A second end attempt, or use of a stale handle, can throw because that touch no longer exists.
- Movement is not one-to-one with page events. Browser optimizations can throttle or coalesce touch movement, so each
touch.move()call is not guaranteed to produce a separate page-observedtouchmove. - Cleanup is best effort. A
finallyblock ensures your code attempts the release after an error, but cannot guarantee recovery if the browser or protocol itself has failed.
5. Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| An error says the touch does not exist when moving or ending it | The touch has already ended, the handle is stale, or another call ended the first active touch. | Track the handle from the matching touchStart(), end it only once, and check whether touchscreen.touchEnd() already released it. |
| The page does not respond as expected to the touch | The coordinates may miss the intended target or touch emulation may not be enabled. | Use viewport coordinates within the target and configure a touch-enabled viewport with hasTouch: true. |
Fewer touchmove events arrive than move calls |
The browser may optimize, throttle, or coalesce movement events. | Do not assume a one-call-to-one-event mapping. Validate the final interaction or position instead of counting move calls. |
| A held touch remains active after an exception | The end call was skipped because it was not protected by cleanup logic. | Put await touch.end() in a finally block, and avoid attempting another end if cleanup already succeeded. |
6. Reliability, performance, and cost
For a tap, tap() has the smallest lifecycle to manage because it encapsulates start and end. For a gesture with a hold or movement, keep the handle in a narrow scope and release it in finally. Touch calls are browser automation commands; avoid adding unnecessary movement calls, and do not treat a series of calls as proof that the page emitted the same number of events.
Puppeteer runs in your browser automation environment, so the practical costs are the compute and browser time your own setup uses. This touch API does not imply a per-touch service charge. If you need screenshots of the resulting page rather than control over its touch interaction, ScreenshotNeo is a website screenshot API and MCP server; it captures pages but does not replace Puppeteer touch control.
7. Or skip the browser setup
If the goal is a screenshot rather than exercising a touch interaction, ScreenshotNeo’s API docs show the one-request capture API. This cURL request returns a WebP screenshot:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
The same request in Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
And in Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
ScreenshotNeo accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, and failed loads are not billed. Its MCP server gives AI agents screenshot tools. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Sign up for the free plan.
8. FAQ
Does touchStart() end the touch automatically?
No. It starts a touch and returns a handle. Call end() on that handle when the gesture is finished.
Should I use touchEnd() or TouchHandle.end()?
Use the handle method to end the specific touch created by touchStart(). The touchscreen method ends the first active touch.
Can I use tap() instead?
Yes, when you need only a tap. It dispatches both touch start and touch end; explicit start and end calls are for gestures that need an intermediate held or moved state.
Will every move call trigger a page touchmove event?
Not necessarily. Browser optimizations can reduce or combine movement events.


