PRACTICAL GUIDE / FluentWait lazy timeout message supplier
Build timeout evidence only when the wait actually fails
Use FluentWait's message supplier to defer diagnostic formatting until timeout, preserve the last poll state, and avoid masking the original failure.
In this guide6 sections
What you will learn
- Where the eager work really happens
- Carry the last observation out of the condition
- Decide what belongs in the snapshot
- Prove the supplier did not replace the failure
Your wait succeeds in 300 milliseconds, yet every call spends two seconds building a screenshot-rich error message. The code passed withMessage(expensiveDiagnostics(driver)), so Java evaluated the method before polling began. A supplier moves that cost to the timeout path, but only if the supplier itself is safe.
Where the eager work really happens
FluentWait has two relevant overloads: withMessage(String) and withMessage(Supplier<String>). The first accepts an already constructed string. Java evaluates method arguments before invoking a method, so this code performs the diagnostic work immediately:
new FluentWait<>(driver)
.withTimeout(Duration.ofSeconds(10))
.withMessage(buildDiagnostics(driver));Selenium did not make the call eager. Java produced the string before withMessage(String) received it. If buildDiagnostics() takes a screenshot, serializes a large DOM fragment, or sends WebDriver commands, every successful wait pays that cost.
The supplier overload changes when the message is evaluated:
new FluentWait<>(driver)
.withTimeout(Duration.ofSeconds(10))
.withMessage(() -> "Checkout was not ready: " + lastObservation.get());Selenium documents that supplier as being evaluated on failure and appended to the timeout message. It is not called on every poll, and it does not influence whether the condition succeeds.
The wait loop repeatedly applies the condition until it returns a value that is neither null nor false. An unignored exception stops the wait immediately. An ignored exception can be retained as the last failure while polling continues. Once the timeout expires, Selenium builds the timeout exception and asks the message supplier for its text.
WebDriverWait extends FluentWait<WebDriver>, so it inherits the supplier overload. It also ignores NotFoundException by default, whereas a newly constructed FluentWait ignores nothing unless configured. Moving code between the two classes without noticing that policy can change an immediate lookup failure into a timeout, even when the message code is identical.
Polling time matters here. The configured interval does not include the cost of evaluating the condition. A condition that spends 700 milliseconds on browser calls cannot truly poll every 100 milliseconds. Lazy message construction removes success-path message cost, but it cannot repair an expensive condition.
Carry the last observation out of the condition
The useful pattern is to collect a small snapshot from values the condition already needed, then let the supplier format that snapshot. Do not make the supplier revisit the page after the last failed poll.
This JUnit 5 example contains both paths. The first button becomes enabled before timeout, so the supplier is never evaluated. The second locator times out, and the resulting message includes the last observed match count and state.
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.FluentWait;
class LazyWaitMessageTest {
private final WebDriver driver = new ChromeDriver();
record Observation(
long elapsedMillis,
int matches,
boolean displayed,
boolean enabled
) {}
@AfterEach
void stopBrowser() {
driver.quit();
}
@Test
void evaluatesTheMessageOnlyOnTimeout() {
String html = """
<button id="place-order" disabled>Place order</button>
<script>
setTimeout(() => {
document.getElementById('place-order').removeAttribute('disabled');
}, 250);
</script>
""";
String page = "data:text/html;base64," + Base64.getEncoder()
.encodeToString(html.getBytes(StandardCharsets.UTF_8));
driver.get(page);
AtomicInteger successMessages = new AtomicInteger();
AtomicReference<Observation> successState = new AtomicReference<>();
WebElement button = waitForReady(
By.id("place-order"),
Duration.ofSeconds(2),
successState,
successMessages
);
assertEquals(0, successMessages.get());
assertTrue(button.isEnabled());
AtomicInteger timeoutMessages = new AtomicInteger();
AtomicReference<Observation> timeoutState = new AtomicReference<>();
TimeoutException failure = assertThrows(
TimeoutException.class,
() -> waitForReady(
By.id("missing-button"),
Duration.ofMillis(350),
timeoutState,
timeoutMessages
)
);
assertEquals(1, timeoutMessages.get());
assertTrue(failure.getMessage().contains("matches=0"));
}
private WebElement waitForReady(
By locator,
Duration timeout,
AtomicReference<Observation> last,
AtomicInteger messagesBuilt
) {
long started = System.nanoTime();
FluentWait<WebDriver> wait = new FluentWait<>(driver)
.withTimeout(timeout)
.pollingEvery(Duration.ofMillis(100))
.ignoring(StaleElementReferenceException.class)
.withMessage(() -> {
messagesBuilt.incrementAndGet();
return "Control did not become ready; locator=" + locator
+ ", last=" + last.get();
});
return wait.until(current -> {
List<WebElement> matches = current.findElements(locator);
WebElement candidate = matches.size() == 1 ? matches.get(0) : null;
boolean displayed = candidate != null && candidate.isDisplayed();
boolean enabled = candidate != null && candidate.isEnabled();
long elapsed = Duration.ofNanos(
System.nanoTime() - started
).toMillis();
last.set(new Observation(
elapsed,
matches.size(),
displayed,
enabled
));
return displayed && enabled ? candidate : null;
});
}
}The AtomicReference is a mutable holder for an immutable record. The wait and supplier run on the calling thread in this example, so an ordinary holder object could also work. Atomics make the handoff explicit, but they do not make FluentWait safe to share between test threads. Keep one wait and one driver owner per test flow.
The counter demonstrates laziness; production code does not need it. Avoid depending on a specific number of supplier invocations as business behavior. The useful contract is that successful waits do not need the timeout text, while a timeout can include the last snapshot.
Decide what belongs in the snapshot
A diagnostic snapshot should answer why the condition remained false. For a button, match count, displayed state, enabled state, elapsed time, and perhaps a stable application state attribute are usually enough. For a network-backed status panel, the latest visible status text or request correlation ID may matter more.
Reuse values already collected by the condition. Calling findElements() once and recording its size is cheap relative to calling it again only for logging. If the condition reads a status attribute to decide readiness, store that value in the same poll.
Do not turn every poll into an artifact dump. Screenshots, page source, accessibility trees, and large JavaScript objects multiply wait cost by the poll count. A ten-second wait at 100-millisecond intervals can run the condition roughly one hundred times when its work is fast. Even a modest extra command becomes material on a remote Grid.
Sensitive data also belongs outside messages. Selenium exceptions flow into CI logs, test reports, chat notifications, and retained artifacts. Record only the state needed for diagnosis, redact tokens and customer values, and cap user-visible text rather than embedding full DOM.
There is a trade-off between precision and overhead. A lightweight snapshot is slightly less complete than a timeout-time page dump, but it represents the state observed by the condition and cannot issue a new browser command that changes the failure. Capture heavier artifacts once in the catch block or the test framework's failure hook.
Choose snapshot fields per condition rather than building a universal object. A universal snapshot tends to query URL, title, DOM, network state, and screenshots whether the wait concerns any of them. A small condition-specific record is faster, easier to redact, and clearer in a timeout. The cost is several tiny record types, which is preferable to one expensive diagnostic collector hidden behind every wait.
That later screenshot is not perfectly simultaneous with the last poll. Name it as timeout-time evidence, not final-poll evidence. The snapshot holds the causal condition state; the screenshot supplies visual context.
Prove the supplier did not replace the failure
Run the focused test and inspect the full TimeoutException:
mvn -Dtest=LazyWaitMessageTest#evaluatesTheMessageOnlyOnTimeout \
-DtrimStackTrace=false testThe custom text should appear alongside Selenium's attempted timeout and polling interval. If the condition threw an ignored exception on its final attempts, inspect the cause chain as well. That exception may explain a re-render or transient lookup failure better than the Boolean snapshot alone.
A supplier that calls WebDriver creates a second failure boundary:
.withMessage(() -> "URL=" + driver.getCurrentUrl()
+ ", source=" + driver.getPageSource())At timeout, the session may already be invalid, an alert may block commands, or navigation may be in progress. If the supplier throws, that new exception can prevent the intended timeout diagnostic from being constructed. Even when it succeeds, it observes the page after the final condition evaluation, not necessarily the state that made the condition false.
Test the timeout path deliberately. Many teams test only the success case, so broken suppliers remain dormant until CI is already failing. A small unit or browser test with a short timeout should verify the custom text, last observation, cause preservation, and artifact hook behavior.
Also verify the success path does not execute the supplier. Use a counter around genuinely expensive formatting in a focused test, then remove the counter from production. Measuring suite duration alone can miss the issue because Grid startup and navigation noise are much larger.
Keep exception handling narrow
Ignoring StaleElementReferenceException can be reasonable when the component is known to replace the candidate during rendering and the condition relocates it on every poll. It has a cost: repeated staleness becomes a timeout instead of surfacing at the first occurrence. The last observation and retained cause must make that behavior visible.
Do not ignore WebDriverException broadly. That family includes failures such as an invalid session or browser communication problem that another poll cannot heal. Converting them into a ten-second timeout delays the result and mislabels infrastructure failure as application readiness.
findElements() is useful for readiness conditions because zero matches returns an empty list instead of throwing NoSuchElementException. This lets the snapshot record matches=0 without adding another ignored exception. More than one match should usually remain false and be reported, because silently choosing the first makes ambiguity look like readiness.
An unignored exception exits immediately, so the timeout supplier is not evaluated. That is correct. The custom timeout message describes a condition that stayed unsatisfied until its deadline, not every possible way the wait call can fail.
When a lazy message is the wrong tool
Use the String overload for a cheap, static statement such as "Checkout button did not become enabled". A supplier adds indirection without value when there is no deferred work or changing snapshot.
Do not use the message supplier as a per-poll logger. It runs on the timeout path, not as an observer. If every transition matters, instrument the condition deliberately with bounded structured records and accept the runtime and storage cost.
Avoid embedding recovery actions in the supplier. Refreshing the page, dismissing an alert, or closing a modal while constructing an exception changes the system after the wait has failed. Recovery belongs in an explicit higher-level flow where its side effects and retry policy can be tested.
Do not move all diagnostics out of the condition if doing so removes the state needed to understand readiness. The goal is not zero work per poll; the goal is no duplicate or unrelated work. Collect the values the decision already requires and defer only their human-readable formatting.
Finally, do not add an elaborate supplier to compensate for a vague condition. until(driver -> isReady()) with no observable state will still produce a mysterious timeout. Name the readiness contract, record its last relevant values, and let the message explain the unmet condition in one line.
// FIELD DISPATCH
Get the QA Field Notes
Weekly QA battles, AI testing guides, and interview drills. Free on Substack.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 02Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 03Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 04
FAQ / QUICK ANSWERS
Questions testers ask
When does FluentWait evaluate a Supplier<String> message?
The supplier is evaluated when the wait times out, so a successful wait does not build that message. Selenium appends the supplied text to its timeout failure rather than using it as a polling condition.
What is the difference between withMessage(String) and withMessage(Supplier<String>)?
Java evaluates a String argument before withMessage() is called, including any method used to construct it. The Supplier overload stores work that can be performed later on the failure path.
Should a timeout message supplier call WebDriver?
Avoid additional browser commands there. A failed session, alert, navigation, or supplier exception can replace the useful TimeoutException, while the page may already differ from the final observation.
How can a timeout message include the last state Selenium saw?
Update a small immutable snapshot inside the wait condition using values the poll already collected. Let the supplier format that snapshot without querying the browser again.
Which exceptions should FluentWait ignore while polling?
Ignore only exceptions that are expected to be transient for that condition, such as a narrowly understood stale element during re-render. Connection failures, invalid sessions, and broken selectors should usually stop immediately.
RELATED GUIDES
Continue the learning route
GUIDE 01
Debug Selenium Grid Queue Timeouts and Stereotype Mismatches
Resolve Selenium Grid queue timeouts by comparing queued capabilities with live slot stereotypes, separating capacity pressure from impossible matching.
GUIDE 02
A 60-Day SDET Coding Interview Roadmap for Java Beginners
SDET Coding Interview Roadmap for Java Beginners interview guide with realistic scenarios, model-answer guidance, scoring, common mistakes, and practical.
GUIDE 03
Add Selenium Accessibility Testing to User Journeys
Master Selenium accessibility testing with practical examples, architecture decisions, failure analysis, CI guidance, metrics, and scenario-led interview answers.
GUIDE 04
Advanced Java Automation Framework Interview Questions
advanced Java automation interview questions: practical design, implementation, debugging, CI, metrics, and interview guidance for QA, SDET, and automation engineers.
GUIDE 05
Build a Selenium Page Component Model for Large Suites
Master Selenium page component model with practical examples, architecture decisions, failure analysis, CI guidance, metrics, and scenario-led interview answers.