Handles
Handles are references to JavaScript objects (JSHandle) or DOM elements (ElementHandle) that live in the browser. I almost never use ElementHandle directly anymore — Locators replaced them and are much better. The one case I still reach for JSHandle: when I need to hold a reference to a browser-side object across multiple evaluate() calls without serializing it each time.
Locator vs ElementHandle — why Locator wins
An ElementHandle points to a specific DOM node captured at a moment in time. If React re-renders and replaces that node with a new one, the handle is stale — it still points to the old detached node. This causes subtle bugs where actions succeed but operate on a ghost element.
A Locator stores the query logic, not the element. Every time I call .click(), .fill(), or any assertion, Playwright re-queries the DOM fresh. This means locators work correctly even after React re-renders, navigation within a SPA, or any DOM mutation.
JSHandle — when I actually use it
A JSHandle is a reference to any JavaScript object in the browser — including non-DOM objects like window, arrays, or complex objects from the page's JavaScript environment. The object stays alive in the browser; the handle is just a pointer from Node.js.
The use case where JSHandle saves multiple round-trips: when I create a large JavaScript object in the browser and want to call multiple operations on it without serializing and deserializing the whole thing over the protocol each time.
ElementHandle — legacy, use Locator instead
The old way to interact with elements before Locators existed. page.$() returns an ElementHandle (equivalent to document.querySelector). I might see this in older codebases — the migration path is to replace page.$() calls with page.locator() or getBy* locators.
The one remaining legitimate use: ElementHandle.boundingBox() to get an element's pixel coordinates for visual assertions or manual gesture simulations. Locators don't expose boundingBox() directly.
Handle lifecycle — dispose when done
Handles keep the referenced JavaScript object alive in the browser, preventing garbage collection. When I'm done with a handle, I should call dispose() to release it. If the page navigates, all handles become invalid automatically.