In Playwright, when is passing an ElementHandle into page.evaluate() better than locator.evaluate()?
answer
- One element versus several
- The locator form disposes for you
- Handles pass through the argument live
- Arrays and object properties both work
- Return values are serialised
basics
~20 sUse locator.evaluate() for one element: it resolves the locator, runs your function with that element and disposes the handle for you. Pass handles into page.evaluate() only when one function needs several live objects at once.
solid answer
~40 s`locator.evaluate(fn)` waits for the locator to resolve to one attached element, runs `fn` in the page with that element as the first argument, and disposes the temporary handle when it finishes -- you never hold the handle, so you cannot leak it. `page.evaluate(fn, arg)` serialises its argument, but an `ElementHandle` or `JSHandle` anywhere inside `arg` is passed through live, alone, in an array or as object properties. That is the case for handles: comparing two nodes, such as the To-do and Done columns of an issue board, or mixing an element with an object obtained from `page.evaluateHandle()`, in one round trip. The costs are then yours: dispose each handle, expect `JSHandle is disposed!` if you reuse one afterwards, and remember that `page.evaluate()` serialises its return value, so a DOM node cannot come back out.
code
typescript · 17 lines// One element: no handle appears anywhere.
const board = page.getByRole('region', { name: 'Board' });
const rowCount = await board.evaluate(el => el.querySelectorAll('[data-row]').length);
// Two live elements in a single round trip: handles are the mechanism.
const todo = await page.getByRole('list', { name: 'To do' }).elementHandle();
const done = await page.getByRole('list', { name: 'Done' }).elementHandle();
try {
const sameWidth = await page.evaluate(
([a, b]) => a.getBoundingClientRect().width === b.getBoundingClientRect().width,
[todo, done],
);
expect(sameWidth).toBe(true);
} finally {
await todo.dispose();
await done.dispose();
}go deeper
Reach for locator.evaluate when you need a property the API does not expose. It gives your function the element and cleans up after itself, so no handle enters your code.
Explain the argument rule: values are serialised, but handles inside the argument arrive live, which is why several elements can meet inside one evaluate call.
Weigh the round trip against the ownership cost, keep handle creation as late as possible with disposal in a finally, and know the disposed and cross-document errors when you see them.
Decide how much page-internal evaluation belongs in a suite at all, since every evaluate couples tests to implementation detail that the user-facing locator APIs deliberately hide.
## The two shapes, stated plainly `locator.evaluate(fn)` and `page.evaluate(fn, handle)` both end with your function running inside the browser with a real DOM element as an argument. The difference is who owns the reference in between. `locator.evaluate()` resolves the locator itself -- strictly, waiting for the element to be attached -- calls your function with that element, and disposes the temporary handle before returning. You never hold the handle, so you cannot leak it. `page.evaluate(fn, arg)` serialises its argument, with one exception: an `ElementHandle` or `JSHandle` anywhere in `arg` is passed through live, as the real object in the page. That exception is the entire reason to reach for a handle here. ## How handles travel into an evaluate - alone: `page.evaluate(el => el.tagName, handle)`; - inside an array, destructured in the function: `([a, b]) => ...`; - as object properties, where the property names must match the destructuring; - mixed freely with ordinary serialisable values in the same argument. ## When a handle genuinely earns its place 1. **Two or more live elements in one call** -- comparing the geometry of the To-do and Done columns of an issue board in a single round trip, rather than reading each separately and racing between the reads. 2. **A non-element object from the page** -- a `JSHandle` for a store or a widget instance obtained with `page.evaluateHandle()`, handed back into a later evaluate. 3. **Extensive traversal on a static surface**, the one case the documentation still endorses for `ElementHandle`. Everything else -- one element, one property -- is `locator.evaluate()` work, and for all matching elements at once there is `locator.evaluateAll()`, which passes the array of elements without you touching a handle at all. ## The costs you take on | | `locator.evaluate()` | handle + `page.evaluate()` | |---|---|---| | Resolution | strict, waits for attached | already fixed at creation | | Handle lifetime | disposed for you | yours until `dispose()` | | Several elements at once | one element only | as many as you pass | | After a re-render | resolves the new node | passes a detached node | | Failure you can cause | none extra | `JSHandle is disposed!` after disposal | Two error messages are worth recognising here. Using a handle after `dispose()` fails with `JSHandle is disposed!`. Handles are also document-scoped: Playwright reports `Unable to adopt element handle from a different document` when one is used against a document it does not belong to, which is what a navigation between capture and use tends to produce. ## What comes back out `page.evaluate()` serialises its **return** value, so a DOM node cannot come back -- a non-serialisable return resolves to `undefined`. When you want the live object back, the handle-returning forms are `page.evaluateHandle()` and `locator.evaluateHandle()`, and whatever they give you is yours to dispose. Playwright does transfer a few values plain `JSON` cannot, including `NaN`, `Infinity`, `-Infinity` and `-0`, but a live element is not among them. ## A rule of thumb Reach for `locator.evaluate()` by default; it is the same access with the ownership problem removed. Escalate to explicit handles only when one function must see more than one live object at the same instant, and when you do, create both handles as late as possible, run the single evaluate, and dispose them in a `finally`. That keeps the window in which a re-render could invalidate them down to the width of one call.
- What happens to the handle that locator.evaluate() resolves internally?Playwright resolves the locator to a temporary handle, runs your function with it, and disposes that handle before the call returns. The node is therefore not pinned against garbage collection and there is nothing for you to clean up, which is the main reason to prefer this form over taking a handle yourself.
- Can page.evaluate() hand a DOM element back to the test?No. Its return value is serialised, and a non-serialisable value such as a DOM node resolves to `undefined`. Use `page.evaluateHandle()` or `locator.evaluateHandle()` when you want the live object back as a handle, and dispose whatever they give you once you are done with it.
saying these in an interview costs you the question
- page.evaluate can return a DOM node to the test
- locator.evaluate leaks a handle you must dispose
- Handles cannot be passed as evaluate arguments
- locator.evaluate runs the full readiness checks first
- A disposed handle still works inside the page