What does Playwright's locator.elementHandle() do before it returns a handle?
answer
- It is waitForSelector underneath
- Attached, not visible
- Strict resolution to one element
- TimeoutError when nothing appears
- You own the returned handle
basics
~20 sIt waits for the locator to resolve to exactly one attached element and returns a handle to that DOM node. It waits for attachment only, not visibility, and throws when several elements match or when the timeout expires.
solid answer
~40 sUnderneath, `locator.elementHandle()` is `waitForSelector` with `strict: true` and `state: 'attached'`. So it does wait -- but only for the node to be in the document, not for it to be visible, stable or enabled; a `display: none` row satisfies it. Several matches throw, because resolution is strict, and nothing matching inside the timeout throws a `TimeoutError`. The handle you get back is yours: it pins that node against garbage collection until `dispose()`, it is auto-disposed when the frame navigates, and it stops working the moment the node is replaced. The plural `locator.elementHandles()` behaves differently -- it returns every current match immediately, with no wait and no single-match rule, possibly an empty array. Both are marked discouraged; `locator.evaluate()` does the same resolution and disposes for you.
code
typescript · 10 linesconst composer = page.getByRole('textbox', { name: 'Add a comment' });
// Waits for the node to be attached, then freezes on it.
const handle = await composer.elementHandle({ timeout: 5000 });
try {
const grew = await handle.evaluate(el => el.scrollHeight > el.clientHeight);
expect(grew).toBe(true);
} finally {
await handle.dispose();
}go deeper
Know that the call hands you a fixed reference to one element, and that you have to dispose it afterwards. Prefer using the locator directly until you have a reason not to.
Be able to name the mechanics: strict resolution, state attached, a TimeoutError when nothing appears, and a handle whose lifetime you now own.
Judge when a handle is worth it at all, keep it to a single step with a finally block for disposal, and recognise the hidden-element trap in waiting only for attachment.
Decide the team rule for handle use and its escape hatch, so that a discouraged API needs a justification in review rather than spreading through shared helpers.
## The call is `waitForSelector` in disguise `locator.elementHandle()` resolves the locator by running `waitForSelector` with `strict: true` and `state: 'attached'`, then returns the resulting `ElementHandle`. Every surprising thing about the method follows from that one line: it polls, it insists on a single match, it stops at attachment, and it hands you a reference you now own. ## What it waits for, and what it does not The state is `attached`, not `visible`. In practice that means: - a row rendered with `display: none` satisfies the call -- you get a handle to a hidden element; - an element that exists but is covered, disabled or mid-animation satisfies it too; - an element that has not been rendered yet does **not** -- the call keeps polling and eventually throws a `TimeoutError` naming the selector. So the method does wait, but only for the weakest of the conditions an action would require. A handle obtained this way is not a promise that the element is clickable; it is only a promise that the node was in the document at that moment. ## Three ways it ends badly 1. **Nothing matches in time.** A `TimeoutError` is thrown; `elementHandle({ timeout: 5000 })` sets the budget for this one call. 2. **More than one element matches.** The resolution is strict, so the call throws rather than quietly taking the first match. Narrow the locator before asking for a handle. 3. **The node dies afterwards.** Nothing fails at creation time; the cost lands later, when an action through the handle throws `Element is not attached to the DOM`. ## The plural sibling is a different animal | | `locator.elementHandle()` | `locator.elementHandles()` | |---|---|---| | Waits | yes, for `attached` | no, snapshots immediately | | Several matches | throws | returns all of them | | No match | `TimeoutError` | empty array | | Returns | one `ElementHandle` | an array of `ElementHandle` | The plural form is a bare query: whatever the issue board happens to contain at that microsecond, wrapped in handles. If the list is still loading you get a short array or an empty one, with no error to tell you so. ## The handle is now your responsibility - It pins the node against garbage collection until `dispose()` is called on it. - It is auto-disposed when its owning frame navigates, so a handle taken before a `page.goto()` is dead afterwards. - It never re-resolves, so it ages badly on any surface that re-renders. - Reusing one after `dispose()` fails with `JSHandle is disposed!`. A `try`/`finally` around the use is the honest shape when you do take one, because an assertion failure in between would otherwise leak the reference for the rest of the test. ## Prefer the call that cleans up after itself `locator.evaluate(fn)` performs the same resolution -- strict, `attached` -- runs your function in the page with the element as its first argument, and then disposes the temporary handle for you before returning. For reading a property off one element it is strictly better: same wait, no ownership, no leak, one fewer thing to review. Both `elementHandle()` and `elementHandles()` are flagged as discouraged in the API docs precisely because the locator-shaped alternatives exist for almost everything people reach for them to do.
- Does locator.elementHandle() wait for the element to be visible?No. It waits with state `attached` only, so a node rendered with `display: none` satisfies it and you get a handle to a hidden element. If visibility matters, assert it first with `expect(locator).toBeVisible()`, or act through the locator, since locator actions run the full readiness checks themselves.
- What does locator.elementHandles() return when nothing matches?An empty array, immediately. The plural form does not wait and does not apply the one-match rule -- it snapshots whatever is present at that instant. An empty result therefore may only mean the issue board had not finished rendering, and nothing in the call tells you so.
saying these in an interview costs you the question
- elementHandle waits for the element to be visible
- It returns null when nothing matches
- Handles returned this way clean themselves up
- The plural elementHandles waits like the singular form
- Taking a handle first makes later actions safer