Q6 of 42 · Playwright

What is a Locator and how does it differ from an ElementHandle?

PlaywrightJuniorplaywrightlocatorfundamentalsjunior

Short answer

Short answer: A Locator is a *query description* — it re-runs every time you act on it, giving auto-retry. An ElementHandle is a *snapshot reference* to a single DOM node at a point in time. Locators are the modern, recommended API; ElementHandles are legacy and prone to staleness.

Detail

Playwright shipped with ElementHandle (similar to Selenium's WebElement) and later introduced Locator as the preferred abstraction.

ElementHandle is a reference to a specific DOM node, captured at the moment page.$('selector') returned. If the DOM updates (re-render, virtual list scroll, animation), the handle becomes stale and operations on it fail. The classic React staleness problem.

Locator is a query plus a strategy for re-running it. page.locator('selector') doesn't query the DOM until you call an action (.click(), .textContent()). Each action re-queries, with auto-wait for actionability. There's no staleness.

// ElementHandle (legacy) — captures once, can go stale
const button = await page.$('[data-test=submit]');
await page.reload();
await button.click(); // ❌ probably stale

// Locator — re-queries on each use
const button = page.locator('[data-test=submit]');
await page.reload();
await button.click(); // ✅ re-queries the DOM, finds the new button

Locators support chaining: page.locator('[data-test=cart-row]').nth(0).locator('[data-test=qty]') reads the first cart row's quantity input.

The semantic locators are also Locators: page.getByRole('button', { name: 'Submit' }), page.getByTestId('submit'), page.getByText('Welcome'). Same retry semantics, more readable.

When ElementHandle is still useful: rare cases where you need to pass a DOM reference into page.evaluate for browser-side computation. locator.elementHandle() converts when you need to. Otherwise prefer Locator everywhere.

// EXAMPLE

locator-vs-handle.spec.ts

import { test, expect } from '@playwright/test';

test('Locator re-queries on each action', async ({ page }) => {
  await page.goto('/dashboard');

  // Locator: a query description, no DOM query yet
  const cartCount = page.getByTestId('cart-count');

  await expect(cartCount).toHaveText('0');

  // Action that updates the DOM
  await page.getByRole('button', { name: 'Add to cart' }).click();

  // Same locator — re-queries, finds the updated count
  await expect(cartCount).toHaveText('1');
});

// WHAT INTERVIEWERS LOOK FOR

Knowing Locator is lazy and re-queries while ElementHandle is a snapshot, and that semantic locators (`getByRole`, `getByTestId`) are also Locators.

// COMMON PITFALL

Reaching for `page.$()` (returns ElementHandle) out of habit from Puppeteer/Selenium and hitting staleness.