Resource library

QA How-To

Selenium BiDi Capture JavaScript Errors Examples: A Practical Tutorial

Selenium BiDi capture JavaScript errors examples with runnable Java, JUnit 5, filtering, waits, cleanup, and CI-ready failure evidence for modern QA teams.

19 min read | 2,663 words

TL;DR

Enable WebDriver BiDi, register driver.script().addJavaScriptErrorHandler before navigation or interaction, and collect JavascriptLogEntry events in a thread-safe list. Wait for the event, assert its text and source, then remove the handler in teardown.

Key Takeaways

  • Enable BiDi on browser options before creating the WebDriver session.
  • Register addJavaScriptErrorHandler before the action that may throw an error.
  • Collect JavascriptLogEntry events in a thread-safe, test-scoped collection.
  • Wait on a business signal and the event collection instead of using fixed sleeps.
  • Keep uncaught exceptions separate from explicit console.error messages.
  • Remove every handler and quit the driver even when an assertion fails.
  • Filter expected errors narrowly and sanitize evidence before publishing CI artifacts.

Selenium BiDi capture JavaScript errors examples should do more than print a browser message. A dependable test subscribes before the risky action, captures typed JavascriptLogEntry events, waits without racing the browser, verifies the relevant exception, and removes the handler afterward.

This tutorial builds that workflow in Java with Selenium and JUnit 5. For the protocol concepts, browser support model, and the rest of the event-driven APIs, keep the Selenium BiDi automation complete guide open beside this focused implementation.

You will run against a deterministic local HTML fixture first, then turn the same collector into a reusable test utility. The examples distinguish uncaught JavaScript exceptions from console.error, because those signals answer different testing questions. For the synchronization foundation used throughout the examples, review explicit waits in Selenium WebDriver.

What You Will Build

By the end, you will have:

  • A Maven project that starts Chrome with WebDriver BiDi enabled.
  • A local page that produces one synchronous and one asynchronous uncaught error.
  • A JUnit 5 test that captures JavascriptLogEntry values as they arrive.
  • A reusable collector with explicit start, wait, filter, and close behavior.
  • A policy test that fails only on relevant application-owned errors.
  • Diagnostic output suitable for a CI test report after sanitization.

The final design scopes one collector to one driver and one test. That boundary prevents errors from parallel sessions or later tests from contaminating the result.

Prerequisites

Use Java 17 or newer, Maven 3.9 or newer, a current Chrome installation, and Selenium Java 4.46.0 for these examples. Selenium Manager resolves the matching local driver in the normal case, so you do not need to download ChromeDriver or set a system property. Pin versions in your project and validate browser coverage in CI rather than assuming every environment exposes the same BiDi features.

Check the tools:

java -version
mvn -version

Create this layout:

selenium-bidi-errors/
├── pom.xml
└── src/
    └── test/
        └── java/
            └── example/
                ├── BidiJavaScriptErrorTest.java
                └── JavaScriptErrorCollector.java

The examples use Chrome for a concise setup. When your matrix includes other browsers, create the corresponding options with BiDi enabled and run the same contract tests on every supported browser. WebDriver BiDi is a cross-browser standard, but implementation coverage still depends on the installed browser, driver, Selenium version, and Grid node.

Step 1: Create the Selenium BiDi Capture JavaScript Errors Examples Project

Add a pom.xml with Selenium Java, JUnit Jupiter, and a recent Surefire plugin. The compiler release keeps the example aligned with Java 17.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>example</groupId>
  <artifactId>selenium-bidi-errors</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <selenium.version>4.46.0</selenium.version>
    <junit.version>5.13.4</junit.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.seleniumhq.selenium</groupId>
      <artifactId>selenium-java</artifactId>
      <version>${selenium.version}</version>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.5.3</version>
        <configuration>
          <useModulePath>false</useModulePath>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Selenium Java contains the high-level BiDi script API and its typed log entries. JUnit supplies lifecycle hooks and assertions. No CDP dependency is required.

Verify: run mvn -q -DskipTests test. Maven should exit successfully after resolving dependencies. If dependency resolution fails, check repository access and confirm that the XML was copied without missing closing tags.

Step 2: Enable BiDi Before Driver Creation

Create the beginning of BidiJavaScriptErrorTest.java. BiDi is a session capability, so call enableBiDi() before constructing ChromeDriver. Registering event handlers without a BiDi-enabled session cannot create the event channel you need.

package example;

import java.time.Duration;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.bidi.log.JavascriptLogEntry;
import org.openqa.selenium.support.ui.WebDriverWait;

import static org.junit.jupiter.api.Assertions.assertTrue;

class BidiJavaScriptErrorTest {
  private ChromeDriver driver;

  @BeforeEach
  void startBrowser() {
    ChromeOptions options = new ChromeOptions();
    options.enableBiDi();
    driver = new ChromeDriver(options);
  }

  @AfterEach
  void stopBrowser() {
    if (driver != null) {
      driver.quit();
    }
  }
}

Keep browser creation outside page objects. Capabilities cannot be retrofitted after the session starts, and hiding session construction makes failures difficult to diagnose on Grid. Headless execution is optional. Add --headless=new through options.addArguments(...) only when your environment requires it, and test headed and headless modes in your actual matrix.

Verify: temporarily add a test that opens data:text/html,<title>ready</title> and asserts the title. Run mvn test; one Chrome session should open and close, and the build should pass.

Step 3: Register a JavaScript Error Handler Before the Action

Add the first complete test inside the class. The handler receives uncaught JavaScript error events. CopyOnWriteArrayList is suitable for this low-volume test because Selenium may invoke the callback while the test thread reads the collection.

@Test
void capturesAnUncaughtJavaScriptError() {
  List<JavascriptLogEntry> errors = new CopyOnWriteArrayList<>();
  long handlerId = driver.script().addJavaScriptErrorHandler(errors::add);

  try {
    String page = "data:text/html,"
        + "<button id='break' onclick=\"throw new Error('checkout crashed')\">"
        + "Break checkout</button>";
    driver.get(page);
    driver.findElement(By.id("break")).click();

    new WebDriverWait(driver, Duration.ofSeconds(5))
        .until(ignored -> errors.stream()
            .anyMatch(error -> error.getText().contains("checkout crashed")));

    assertTrue(errors.stream()
        .anyMatch(error -> error.getText().contains("checkout crashed")));
  } finally {
    driver.script().removeJavaScriptErrorHandler(handlerId);
  }
}

The order is deliberate: create the session, subscribe, navigate, act, wait, assert, and unsubscribe. If you register after the click, the event is already gone. BiDi is an event stream, not a historical buffer.

The try/finally block removes the specific handler even if the wait or assertion fails. The outer @AfterEach still quits the driver. This two-level cleanup prevents callbacks from leaking when a class later reuses a session.

Verify: run mvn -Dtest=BidiJavaScriptErrorTest#capturesAnUncaughtJavaScriptError test. The test should pass. Change checkout crashed in the assertion to different message; the wait should time out, proving that the test reacts to the captured error rather than merely to the click.

Step 4: Capture an Asynchronous JavaScript Exception Without Sleeping

Front-end failures often occur inside timers, promise continuations, event callbacks, or rendering work after the Selenium command returns. A fixed sleep is both slow and unreliable. Wait for the event collection with a bounded timeout instead.

@Test
void capturesAnAsynchronousJavaScriptError() {
  List<JavascriptLogEntry> errors = new CopyOnWriteArrayList<>();
  long handlerId = driver.script().addJavaScriptErrorHandler(errors::add);

  try {
    String page = "data:text/html,"
        + "<button id='load' onclick=\""
        + "setTimeout(() => { throw new Error('profile render failed'); }, 50)"
        + "\">Load profile</button>";
    driver.get(page);
    driver.findElement(By.id("load")).click();

    JavascriptLogEntry captured = new WebDriverWait(
        driver, Duration.ofSeconds(5))
        .until(ignored -> errors.stream()
            .filter(error -> error.getText().contains("profile render failed"))
            .findFirst()
            .orElse(null));

    System.out.printf("level=%s text=%s source=%s%n",
        captured.getLevel(), captured.getText(), captured.getSource());
    assertTrue(captured.getText().contains("profile render failed"));
  } finally {
    driver.script().removeJavaScriptErrorHandler(handlerId);
  }
}

The wait returns the matching entry rather than only a Boolean. That makes later assertions and artifact creation clearer. In a real application, also wait for the customer-visible outcome when one exists. Event capture explains a failure, but it should not replace assertions that the page completed the intended workflow.

Do not assert that errors.size() equals one unless the fixture truly controls every script. Browser pages can generate unrelated events, and production apps may throw more than one error after a root failure. Match the stable application message, source, or stack characteristics that define the contract.

Verify: run the method by name. The test output should contain profile render failed, and the test should finish without a hardcoded delay. Increase the timer from 50 to 500 milliseconds; the explicit wait should still pass.

Step 5: Separate Uncaught Errors from console.error Messages

An uncaught exception and an explicit console call are not equivalent. throw new Error('broken') is a runtime failure if it escapes. console.error('retrying') is an application-selected log message and might be expected. Selenium exposes separate high-level handlers so you can preserve that distinction.

Signal Selenium handler Typed entry Typical policy
Uncaught JavaScript exception addJavaScriptErrorHandler JavascriptLogEntry Fail when application-owned
console.log, warn, or error call addConsoleMessageHandler ConsoleLogEntry Filter by level and intent
Network request or response BiDi network handlers Network event types Assert transport behavior separately
DOM or business result WebDriver locator and assertion Element state Keep as the primary feature assertion

Use both handlers only when the test needs both signals:

List<JavascriptLogEntry> exceptions = new CopyOnWriteArrayList<>();
var consoleMessages = new CopyOnWriteArrayList<
    org.openqa.selenium.bidi.log.ConsoleLogEntry>();

long exceptionId = driver.script().addJavaScriptErrorHandler(exceptions::add);
long consoleId = driver.script().addConsoleMessageHandler(consoleMessages::add);
try {
  driver.get("data:text/html,<script>"
      + "console.error('recoverable API retry');"
      + "setTimeout(() => { throw new Error('uncaught render crash'); }, 0);"
      + "</script>");

  new WebDriverWait(driver, Duration.ofSeconds(5))
      .until(ignored -> !exceptions.isEmpty() && !consoleMessages.isEmpty());
} finally {
  driver.script().removeJavaScriptErrorHandler(exceptionId);
  driver.script().removeConsoleMessageHandler(consoleId);
}

A console message can be valuable evidence without being a release-blocking defect. Store the two collections separately, name them precisely in reports, and evaluate them with different rules.

Verify: print both collection sizes and texts. You should see one explicit console entry and one uncaught exception entry. If you see only one category, confirm that both handlers were registered before navigation.

Step 6: Build a Reusable, Test-Scoped Collector

Move handler lifecycle and waiting into JavaScriptErrorCollector.java. The utility accepts the ChromeDriver used in this tutorial because its script() method provides the high-level API directly. If your framework supports multiple BiDi-capable driver types, wrap construction behind a small tested adapter instead of scattering casts.

package example;

import java.time.Duration;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Predicate;
import org.openqa.selenium.bidi.log.JavascriptLogEntry;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.WebDriverWait;

public final class JavaScriptErrorCollector implements AutoCloseable {
  private final ChromeDriver driver;
  private final List<JavascriptLogEntry> errors =
      new CopyOnWriteArrayList<>();
  private final long handlerId;

  public JavaScriptErrorCollector(ChromeDriver driver) {
    this.driver = driver;
    this.handlerId = driver.script().addJavaScriptErrorHandler(errors::add);
  }

  public JavascriptLogEntry waitFor(
      Predicate<JavascriptLogEntry> match, Duration timeout) {
    return new WebDriverWait(driver, timeout)
        .until(ignored -> errors.stream()
            .filter(match)
            .findFirst()
            .orElse(null));
  }

  public List<JavascriptLogEntry> snapshot() {
    return List.copyOf(errors);
  }

  @Override
  public void close() {
    driver.script().removeJavaScriptErrorHandler(handlerId);
  }
}

Use try-with-resources so cleanup is visible at the call site:

@Test
void collectorCapturesTheRelevantError() {
  try (JavaScriptErrorCollector collector =
           new JavaScriptErrorCollector(driver)) {
    driver.get("data:text/html,<script>"
        + "setTimeout(() => { throw new Error('cart total failed'); }, 0);"
        + "</script>");

    JavascriptLogEntry error = collector.waitFor(
        entry -> entry.getText().contains("cart total failed"),
        Duration.ofSeconds(5));

    assertTrue(error.getText().contains("cart total failed"));
  }
}

Do not make the collection static. Parallel tests would mix sessions and create order-dependent failures. One collector per test gives the event stream a clear owner. snapshot() returns an immutable copy so report code cannot mutate live state.

Verify: run the complete class twice with mvn test. Both runs should pass, and no second-run event should contain data from the first browser session.

Step 7: Add an Application Error Policy and Useful Evidence

A blanket rule such as fail if any error exists is safe only for a controlled fixture. Real applications include third-party widgets, intentional negative scenarios, and known transitional defects. Define an explicit predicate for application ownership, then keep exceptions narrow and reviewable.

private static boolean isBlockingApplicationError(
    JavascriptLogEntry entry) {
  String text = entry.getText();
  String source = String.valueOf(entry.getSource());

  boolean ownedByApp = source.contains("app.example.test")
      || text.contains("checkout")
      || text.contains("cart");
  boolean reviewedException = text.contains("known analytics sandbox error");

  return ownedByApp && !reviewedException;
}

Evaluate a snapshot after the business action completes:

List<JavascriptLogEntry> violations = collector.snapshot().stream()
    .filter(BidiJavaScriptErrorTest::isBlockingApplicationError)
    .toList();

assertTrue(violations.isEmpty(), () ->
    "Unexpected application JavaScript errors:\n" +
    violations.stream()
        .map(entry -> entry.getLevel() + " | " + entry.getText())
        .reduce("", (left, right) -> left + right + "\n"));

Avoid broad allowlist fragments such as TypeError or Failed. They can hide new regressions. A production exception should identify a stable signature, include an issue reference in code review context, and have an owner or expiry. Prefer fixing the page over growing the list.

Before attaching evidence, remove tokens, signed query parameters, emails, and customer values. The exact sanitizer depends on your application. Preserve level, sanitized message, source origin, test phase, browser version, and relevant stack information. Do not let artifact creation throw over the original business assertion.

Verify: add one owned error and one reviewed error to a local fixture. The predicate should return one violation. Change the reviewed message slightly; the test should fail, proving that the exception is narrow rather than a general bypass.

Step 8: Run Selenium BiDi Capture JavaScript Errors Examples in CI and Grid

Run the test locally first:

mvn clean test

In CI, pin the browser image and Selenium dependency, record the browser version, and execute a small BiDi preflight before a large suite. A remote session must request BiDi during creation, and the Grid node plus browser must support it. A local pass does not prove the remote image has identical event coverage.

Keep callbacks lightweight. Add entries to a bounded or appropriate concurrent collection, then format artifacts on the test thread. Screenshots, network calls, and complex assertions inside the callback can delay event processing and make failures difficult to reproduce. For very noisy applications, use a bounded queue and record that truncation occurred. If your runner executes multiple classes at once, apply a deliberate Selenium parallel test execution strategy so each collector remains bound to one session.

Coordinate this error collector with other BiDi subscriptions. If the failure depends on API traffic, follow the Selenium BiDi intercept network requests Python tutorial for the network interception pattern. If timing changes under poor connectivity, use the Selenium BiDi emulate network conditions tutorial to reproduce it deliberately.

Verify: run the same test in the CI browser image and confirm that the known fixture error appears in the test report. Then run two test classes in parallel and verify that each artifact contains only its own session data. Treat a missing known event as an environment or capability failure, not as a passing no-error result.

Troubleshooting

Problem: No JavaScript errors are captured -> Confirm that options.enableBiDi() runs before new ChromeDriver(options). Register the handler before navigation or clicking. Prove the channel with a deterministic uncaught setTimeout(() => { throw new Error('probe'); }, 0) fixture.

Problem: console.error appears, but the JavaScript error list stays empty -> This is expected. console.error is a console message, not an uncaught exception. Add addConsoleMessageHandler if the test policy needs console calls, and keep that data separate.

Problem: The wait times out intermittently -> Wait for the matching event and the relevant business completion signal. Do not read immediately after a click or replace synchronization with a short sleep. Confirm the application catches or transforms the exception, since a caught rejection might not produce an uncaught-error event.

Problem: Events from another test appear -> Remove handlers and avoid static collections. Scope the collector to one driver and one test, close it in finally or try-with-resources, and give every parallel artifact a unique test execution ID.

Problem: The test works locally but not on Grid -> Compare Selenium, browser, driver, and Grid versions. Confirm the remote options request BiDi and run a one-error preflight on the node image. Do not silently convert missing BiDi support into an empty passing collection.

Problem: Teardown hides the original assertion -> Protect diagnostic formatting and handler cleanup so their exceptions do not replace the primary test failure. Always attempt driver.quit() in the outer lifecycle hook, and attach cleanup problems as secondary evidence.

Where To Go Next

You now have a focused error collector. Return to the complete Selenium BiDi automation guide to place script events beside browsing contexts, network events, and production framework design.

Then extend one dimension at a time:

Keep each event collector small and test-scoped. Correlation belongs in a reporting layer that understands timestamps and test phases, not inside protocol callbacks.

Interview Questions and Answers

Q: How do you capture uncaught JavaScript errors with Selenium BiDi in Java?

Enable BiDi on the browser options before creating the driver. Register driver.script().addJavaScriptErrorHandler(...), collect JavascriptLogEntry values in a thread-safe test-scoped collection, and subscribe before the action. Wait for the matching event, then remove the handler by its ID in cleanup.

Q: Why must the handler be registered before navigation or interaction?

BiDi delivers live events while a subscription is active. It does not replay a historical error buffer when a listener is added later. Registering first prevents a race in which the page throws before Selenium begins listening.

Q: What is the difference between a JavaScript error event and console.error?

A JavaScript error event represents an exception that escaped script execution. console.error is an explicit logging call and does not necessarily indicate a thrown exception. Selenium exposes separate handlers, so strong frameworks preserve separate policies and artifacts for them.

Q: How do you make a BiDi error collector safe for parallel tests?

Create one collector per WebDriver session and test. Use a thread-safe collection because the event callback and test thread may access it concurrently, and never store events in a static list. Remove the handler in deterministic cleanup and use unique artifact names.

Q: Should every uncaught JavaScript error fail a Selenium test?

Application-owned uncaught errors usually deserve failure, but the policy must reflect scenario intent and third-party ownership. Use narrow reviewed exceptions rather than broad message suppression. Keep a customer-visible assertion as the primary feature contract and attach the sanitized error as diagnostic evidence.

Q: Why use WebDriverWait instead of Thread.sleep for error capture?

An asynchronous error can arrive at different times across machines. WebDriverWait polls for the exact condition and stops as soon as it succeeds, while a fixed sleep is either wasteful or too short. A bounded explicit wait also produces a clear timeout when the expected event never arrives.

Q: Why prefer BiDi over direct CDP logging for new framework code?

WebDriver BiDi is designed as a cross-browser automation protocol, while CDP is Chromium-specific. Selenium's high-level handlers also avoid coupling test code to low-level protocol event shapes. You must still verify feature support across the actual browser matrix.

Best Practices

  • Enable BiDi during session creation and fail a preflight when the environment cannot deliver a known event.
  • Subscribe before the action and remove every handler by ID.
  • Store uncaught exceptions and console messages in different typed collections.
  • Use one collector per test and session, with no global mutable event state.
  • Wait for exact events and business outcomes instead of sleeping.
  • Match on stable application ownership signals, not only generic error words.
  • Keep allowlist entries narrow, reviewed, and temporary.
  • Sanitize URLs, tokens, personal data, and payload fragments before attachment.
  • Keep callback work small and move formatting or assertions to the test thread.
  • Preserve the original test failure when reporting or cleanup also fails.

Conclusion

These Selenium BiDi capture JavaScript errors examples provide the complete pattern: enable BiDi before session creation, subscribe before the risky action, collect typed entries safely, wait for the relevant event, apply an ownership policy, and clean up deterministically. The result catches runtime failures that ordinary DOM assertions can miss while still preserving the user-visible test contract.

Start with the deterministic local fixture, prove it on every CI browser image, and only then enforce the collector across critical flows. Keep uncaught exceptions separate from console messages, sanitize every artifact, and treat missing expected BiDi events as a capability problem rather than a quiet pass.

Interview Questions and Answers

How would you capture an uncaught JavaScript exception with Selenium BiDi?

I enable BiDi in the browser options before session creation and register `addJavaScriptErrorHandler` before the action. I collect `JavascriptLogEntry` values in test-scoped concurrent storage and explicitly wait for the relevant entry. Finally, I remove the handler by ID and quit the driver without masking the primary assertion.

Why do JavaScript exceptions and console errors need different collectors?

They represent different semantics. An uncaught exception escaped script execution, while `console.error` is an intentional logging call that may describe a handled condition. Separate typed collectors allow different filtering, enforcement, and reporting policies.

How would you avoid race conditions in a Selenium BiDi listener test?

I subscribe before navigation or interaction and use a thread-safe collection. I wait for a predicate matching the expected event rather than reading immediately or sleeping. The listener stays lightweight, and all assertions execute on the test thread.

How would you design JavaScript error capture for parallel execution?

Each driver and test gets its own collector, handler ID, and artifact path. I avoid static mutable lists and return immutable snapshots to reporting code. Cleanup always removes the handler, which prevents events from one lifecycle leaking into another.

What would you include in a JavaScript error CI artifact?

I include the test ID, browser name and version, test phase, event level, sanitized text, source origin, timestamp when available, and useful stack details. I redact tokens, personal data, and signed query parameters. Artifact failures remain secondary so they do not replace the original test failure.

When should an uncaught JavaScript error fail a UI test?

It should fail when it is application-owned, relevant to the test window, and violates the team's error policy. I use precise temporary exceptions for understood third-party or intentional errors, not broad text allowlists. The UI's customer-visible result remains the primary assertion.

Why choose WebDriver BiDi instead of direct Chrome DevTools Protocol events?

BiDi is intended for cross-browser WebDriver automation, while CDP is tied to Chromium. Selenium's high-level API reduces low-level protocol coupling and exposes typed handlers. I still prove support with contract tests on each browser and Grid image used by the team.

Frequently Asked Questions

How do I capture JavaScript errors in Selenium BiDi?

Enable BiDi on the browser options before creating the session, then register `driver.script().addJavaScriptErrorHandler(...)`. Collect the resulting `JavascriptLogEntry` objects, wait for the relevant event, and remove the handler during cleanup.

Does Selenium BiDi capture console.error as a JavaScript exception?

No. An explicit `console.error` call is a console message, while an uncaught thrown error is a JavaScript error event. Use `addConsoleMessageHandler` and `addJavaScriptErrorHandler` separately when you need both signals.

Why is enableBiDi called before new ChromeDriver?

BiDi support is requested as part of WebDriver session creation. Calling `enableBiDi()` on options before constructing the driver establishes the event channel required by the high-level script handlers.

Can Selenium capture asynchronous JavaScript errors?

Yes. Register the error handler before triggering the asynchronous work, then use a bounded explicit wait for a matching entry. This works for an uncaught error raised later by a timer or callback without relying on a fixed sleep.

Is a CopyOnWriteArrayList required for Selenium BiDi listeners?

It is not the only option, but it is convenient for low-volume test events because callbacks can write while the test thread reads. For high event volume, consider a bounded concurrent queue and keep callback processing minimal.

Should Selenium tests fail on every JavaScript error?

Fail on relevant application-owned uncaught errors according to a documented policy. Filter intentional scenarios and approved third-party noise narrowly, retain the customer-visible assertion, and attach sanitized evidence for diagnosis.

Does Selenium BiDi JavaScript error capture work on Grid?

It can work when the Selenium client, Grid, node, driver, and browser all support the requested BiDi feature. Run a deterministic preflight on each CI image and never interpret a missing event channel as proof that the page had no errors.

Related Guides