Why do Selenium teams replace @FindBy proxy fields with By constants and driver.findElement at the call site?
answer
- The proxy field answers calls but never names its By
- DefaultElementLocator keeps the locator private
- WrapsElement returns an element, not a By
- A By constant is reusable; a proxy field is not
- Stack frames point at the handler, not at the step
basics
~20 sA proxy field hides its By, so nothing downstream can reuse the locator and failures surface from inside the proxy. A By constant with findElement at the call site puts the locator back within reach.
solid answer
~40 s`DefaultElementLocator` keeps its `By` private, and the proxy in front of it implements only `WebElement`, `WrapsElement` and `Locatable` — none of which returns a locator. So a page-object method can call the field but cannot describe it: it cannot hand the locator to a `WebDriverWait` condition that takes a `By`, cannot scope a nested search under a component root, and cannot name what it searched for in a message. Failures also arrive from `LocatingElementHandler.invoke` and a `jdk.proxy` frame rather than from the step that broke. Declaring `private static final By PICKUP_WINDOW = By.id("pickup-window")` and calling `driver.findElement(PICKUP_WINDOW)` inside the method makes the locator an ordinary value again. The cost is the declarative field block, the `ByIdOrName` fallback, and `AjaxElementLocatorFactory`'s free polling.
code
java · 26 linesimport org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
public class PickupSchedulerPage {
private static final By PICKUP_WINDOW = By.id("pickup-window");
private static final By SLOTS = By.cssSelector("[data-slot]");
private final WebDriver driver;
public PickupSchedulerPage(WebDriver driver) {
this.driver = driver;
}
public String windowLabel() {
return driver.findElement(PICKUP_WINDOW).getText();
}
public void chooseSlot(int index) {
driver.findElements(SLOTS).get(index).click();
}
public By slotsLocator() {
return SLOTS;
}
}go deeper
Know that a By constant is an ordinary value you can pass around while a @FindBy field is not, and that both look up the element only when the code actually uses it.
Explain what the proxy hides: a private By, no accessor, and only WebElement, WrapsElement and Locatable on the proxy. Be able to name what you lose by moving back, including the ByIdOrName fallback.
Show the diagnostic difference in a real suite: failures framed by LocatingElementHandler versus failures framed by your own method, and where handing a locator onward is what the code actually needed.
Own the mixed policy: which fields keep proxies, which become By constants, and how you keep a page object from drifting into two half-applied conventions nobody can reason about.
## What the proxy keeps to itself `DefaultElementLocator` stores the `By` it built from the annotations in a private field and never exposes it. The proxy in front of it implements `WebElement`, `WrapsElement` and `Locatable`, and none of those returns a locator. So a page-object method that owns a `@FindBy` field owns a thing it can *call*, not a thing it can *describe*. The only route from the field back to the locator is textual and conditional: `LocatingElementHandler` returns `"Proxy element for: "` plus the locator from `toString` **only when the element was not found**, and that is a `String`, not a `By`. `WrapsElement.getWrappedElement()` is the other seam, and it is a narrow one. The handler special-cases it: it runs the lookup and returns the real `WebElement` behind the proxy. That hands you a concrete handle — and from that moment you own its staleness, because the returned element is an ordinary reference with no re-finding behind it. ## Where the missing locator bites - A method cannot hand the field's locator to anything that takes a `By`: a `WebDriverWait` condition built from a locator, a scoped `element.findElement(by)` under a component root, or a log line that names what was searched for. - A method cannot ask whether the element is absent without provoking the failure; a `WebElement` proxy has no "did you find it" question, only calls that throw `NoSuchElementException`. - Using the field as the search context for a child lookup works, but it costs its own `findElement` first, once per child — the parent is re-located every time you scope through it. - The failing frames name a `jdk.proxy` class and `LocatingElementHandler.invoke`, so the trace reads as a page-factory failure rather than as the step that broke. ## The plain-`findElement` shape Moving back means declaring the locator as a value and doing the lookup where the work happens: ```java private static final By PICKUP_WINDOW = By.id("pickup-window"); public String windowLabel() { return driver.findElement(PICKUP_WINDOW).getText(); } ``` The `By` is now an ordinary constant. It can be passed to a condition, composed into a nested search, printed in a message, asserted on in a unit test of the page object, or shared between two methods that need the same region of the laundry pickup scheduler. The lookup happens on the line you wrote, so the stack trace names `windowLabel`. ## What you give up by moving back - The declarative field block that reads as a description of the page — a list of `@FindBy` fields is genuinely good documentation. - The `ByIdOrName` fallback, which lets an unannotated `WebElement` field work by matching the field's own name against `id` or `name`. - `AjaxElementLocatorFactory`'s built-in polling, which arrived free with the proxy and now has to be composed explicitly wherever it was doing real work. - One uniform place where every field's lookup happens, replaced by a lookup at each call site. ## The comparison, side by side | | `@FindBy` proxy field | `By` constant plus `findElement` | |---|---|---| | when the lookup runs | inside the proxy, before every method call | on the line you wrote it | | locator reachable as a value | no | yes | | frame that fails | `LocatingElementHandler.invoke` | your page-object method | | built-in polling | yes, under `AjaxElementLocatorFactory` | no — composed by hand | | unannotated field works | yes, via `ByIdOrName` on the field name | not applicable | | absence check on a single element | only by catching the exception | `findElements(by).isEmpty()` | ## Choose per field, not per suite This is not an all-or-nothing switch, and treating it as one is the usual mistake. 1. Keep proxy fields for the parts of the page a method only ever reads or clicks, where the locator never needs to travel anywhere. 2. Use a `By` constant wherever the locator itself has to be passed on, scoped under a component root, or named in a message. 3. Keep both in the same page object when that is what the page needs; the two mechanisms coexist because `PageFactory.initElements` only replaces fields it recognises and leaves a `private static final By` alone. The judgement is mechanical, not philosophical: ask whether the method needs the locator or only the element.
- Is there any way to get the By back out of a @FindBy proxy field?Not as a By. The locator is private in `DefaultElementLocator` and the proxy exposes no accessor. The only glimpse is textual: when the element is missing, `toString` on the proxy returns `"Proxy element for: "` plus the locator, which prints the By. That is a debugging aid, not a programmatic route.
- What does getWrappedElement() on a proxy field give you?`LocatingElementHandler` special-cases it: it runs the lookup and returns the real `WebElement` behind the proxy. That is a concrete handle with no re-finding behind it, so from that point you own its staleness exactly as if you had called `findElement` yourself.
- Must a page object choose one style for every field?No. PageFactory.initElements only replaces fields it recognises as WebElement or annotated List<WebElement>, and leaves a `private static final By` untouched. A page object can keep proxy fields for elements it only reads or clicks and use By constants wherever the locator has to be passed on.
saying these in an interview costs you the question
- Thinks the proxy field exposes the By it was built from
- Says findElement returns a self-refreshing handle
- Claims driver.findElement runs when the page object is constructed
- Believes getWrappedElement returns another lazy proxy
- Treats the choice as all-or-nothing for the whole suite