Summary
Every error the Java client raises is constructed on the BiDi receive thread, so its stack trace
contains only transport frames and never the code that issued the command. Combined with
handle-based operations having no selector to name, a failure inside a large method is
unattributable — you cannot tell from the exception which of twenty calls produced it.
Reported as a defect rather than an enhancement because the stack trace is not merely unhelpful, it
is misleading: it points at the transport for a fault that belongs to a caller, which breaks the
basic contract of a thrown exception.
Reproduction
Three unrelated error paths, all on vibium 26.5.31-browser-clouds:
// 1. selector matches nothing
page.waitFor("#definitely-not-present-anywhere", new FindOptions().timeout(1000));
// 2. element operation on a selector that matches nothing
page.find("#also-not-present", new FindOptions().timeout(1000)).click();
// 3. a different error path entirely — bad script
page.evaluate("this is not valid javascript (");
Every one produces a four-frame stack, none of which names the caller:
type : com.vibium.errors.ElementNotFoundException
message : timeout after 1s waiting for '#definitely-not-present-anywhere': element not found
frames : 4
names caller? : false
top frames : [com.vibium.internal.BiDiClient.mapError(BiDiClient.java:230),
com.vibium.internal.BiDiClient.dispatchLine(BiDiClient.java:193),
com.vibium.internal.BiDiClient.receiveLoop(BiDiClient.java:163),
java.base/java.lang.Thread.run(Thread.java:1474)]
Identical shape for page.evaluate with a syntax error (mapError:240 instead of :230).
The case that is genuinely unattributable
Where a selector is passed, the message at least names it, which softens the problem. Operations on
a previously-obtained Element handle have no selector to name, so the message is bare:
com.vibium.errors.ElementNotFoundException: element not found
at com.vibium.internal.BiDiClient.mapError(BiDiClient.java:237)
at com.vibium.internal.BiDiClient.dispatchLine(BiDiClient.java:193)
at com.vibium.internal.BiDiClient.receiveLoop(BiDiClient.java:163)
at java.base/java.lang.Thread.run(Thread.java:1474)
No caller, no selector, no element id. This is what a stale handle produces —
findAll(...) followed by a per-element read, where the page repaints in between.
What it cost, concretely
An intermittent failure at ~25% in an eleven-field payment form took five diagnostic cycles:
- Wrapped the field-fill helper — the exception still arrived unwrapped, which at least proved the
throw came from somewhere else.
- Wrapped the two
selectOption calls that bypassed the first wrapper.
- Added per-field breadcrumb logging to see how far the form got.
- Breadcrumbs showed all eleven fields completed, so the fault was after the form.
- Located it by elimination in a helper that read error text from the resulting page.
With a caller frame, step 1 would have ended it. We have since had to wrap risky calls in named
diagnostics throughout our suite purely to recover information the exception should carry — which is
a fair amount of code whose only job is to answer "where did this come from?".
Suggested fix
Capture the caller's stack at command dispatch and attach it to the exception before it is handed
back to the waiting thread — either as the exception's own stack via setStackTrace, or as a
synthetic cause. Playwright's Java and Node clients both do a version of this, and it costs one
new Throwable() per command on the dispatch path.
A cheaper partial improvement, if the full stack is unattractive: include the originating command
name and any selector/element id in the message for every error, so the handle case stops being
anonymous.
Environment
- vibium
26.5.31-browser-clouds — local build of browser-clouds @ 0935b943
- Java client as a Maven test dependency; Java 25.0.2; Linux x86_64 (RHEL 9)
- Reproduces with a local browser and with a Grid-connected session (Selenium Grid 4.46.0, Chrome 151)
- Not specific to Grid, to navigation, or to any one error type
Summary
Every error the Java client raises is constructed on the BiDi receive thread, so its stack trace
contains only transport frames and never the code that issued the command. Combined with
handle-based operations having no selector to name, a failure inside a large method is
unattributable — you cannot tell from the exception which of twenty calls produced it.
Reported as a defect rather than an enhancement because the stack trace is not merely unhelpful, it
is misleading: it points at the transport for a fault that belongs to a caller, which breaks the
basic contract of a thrown exception.
Reproduction
Three unrelated error paths, all on vibium
26.5.31-browser-clouds:Every one produces a four-frame stack, none of which names the caller:
Identical shape for
page.evaluatewith a syntax error (mapError:240instead of:230).The case that is genuinely unattributable
Where a selector is passed, the message at least names it, which softens the problem. Operations on
a previously-obtained
Elementhandle have no selector to name, so the message is bare:No caller, no selector, no element id. This is what a stale handle produces —
findAll(...)followed by a per-element read, where the page repaints in between.What it cost, concretely
An intermittent failure at ~25% in an eleven-field payment form took five diagnostic cycles:
throw came from somewhere else.
selectOptioncalls that bypassed the first wrapper.With a caller frame, step 1 would have ended it. We have since had to wrap risky calls in named
diagnostics throughout our suite purely to recover information the exception should carry — which is
a fair amount of code whose only job is to answer "where did this come from?".
Suggested fix
Capture the caller's stack at command dispatch and attach it to the exception before it is handed
back to the waiting thread — either as the exception's own stack via
setStackTrace, or as asynthetic cause. Playwright's Java and Node clients both do a version of this, and it costs one
new Throwable()per command on the dispatch path.A cheaper partial improvement, if the full stack is unattractive: include the originating command
name and any selector/element id in the message for every error, so the handle case stops being
anonymous.
Environment
26.5.31-browser-clouds— local build ofbrowser-clouds@0935b943