Halmurat T.
· 8 min read

Selenium's Alert Handling Crashed Our Parallel Suite

Table of Contents

We were running 340 Selenium tests across 8 threads on a healthcare platform when tests completely unrelated to alerts started throwing UnhandledAlertException. A patient search test — no dialogs anywhere in its flow — crashed because a different test on the same thread had triggered a session timeout confirmation dialog and never dismissed it. The WebDriver was blocked, and every subsequent test assigned to that thread failed immediately with the same exception. It took a full day of digging through logs to realize the problem wasn’t in any individual test. It was Selenium’s fundamental approach to alert handling, and it’s incompatible with serious parallel execution.

How Selenium Handles Alerts — And Why It’s a Problem

Selenium treats browser alerts as blocking operations. When a JavaScript alert(), confirm(), or prompt() fires, the browser is locked. You can’t interact with the page, navigate, or do anything until you explicitly switch to the alert and handle it.

Accepting, dismissing, and reading prompt text are straightforward — driver.switchTo().alert() gives you an Alert object with accept(), dismiss(), getText(), and sendKeys(). The divergence that matters is how you handle unexpected dialogs — the ones you didn’t trigger intentionally:

src/test/java/utils/AlertHandler.java
// The "safe" pattern for unexpected alerts
try {
Alert unexpected = driver.switchTo().alert();
unexpected.dismiss();
} catch (NoAlertPresentException e) {
// No alert? Good. Move on.
}

This is where things fall apart in parallel. You’re forced to wrap every potential alert interaction in a try-catch because you can’t predict whether another test on the same thread left a dialog open. The exception handling becomes defensive code against your own test suite.

The experienced Selenium developer’s instinct here is WebDriverWait with ExpectedConditions.alertIsPresent(). And yes, this handles the timing better than a raw switchTo().alert(). But it doesn’t solve the structural problem: in parallel execution, a dialog triggered by Test A can still block the WebDriver session for Test B if they share a thread. The wait makes the handling more reliable — it doesn’t prevent the contamination. You’re still writing defensive code against your own test suite.

The Parallel Execution Nightmare

The healthcare platform had a 15-minute session timeout that triggered a JavaScript confirm() dialog: “Your session is about to expire. Continue?” In single-threaded execution, this never happened — tests ran fast enough that no session sat idle for 15 minutes. But in parallel with 8 threads, some threads stalled during shared test data setup while waiting for database locks. That idle time was enough.

Here’s how the cascade played out:

  1. Thread 3, Test #47 — a lab results test sits idle for 16 minutes during data setup. Session timeout fires a confirm() dialog.
  2. Test #47 doesn’t expect any alert — it’s a read-only search. UnhandledAlertException kills the test.
  3. Thread 3 picks up Test #48 — a patient intake form. The session timeout dialog is still open from #47. WebDriver can’t interact with the page. Another UnhandledAlertException.
  4. Tests #49 through #58 — same thread, same result. The dialog persists until someone handles it.

Our first fix was a band-aid — an @AfterMethod cleanup hook that tried to dismiss any lingering alerts:

src/test/java/base/BaseTest.java
@AfterMethod(alwaysRun = true)
public void cleanupAlerts() {
try {
driver.switchTo().alert().dismiss();
} catch (NoAlertPresentException e) {
// Expected — most tests don't leave alerts open
}
}

This worked, but at a cost. With 340 tests, that’s 340 NoAlertPresentException catches per run — each one costing roughly 200ms of exception handling overhead. That’s over a minute of pure waste just checking for alerts that 95% of tests never trigger. And it still didn’t help when a timeout dialog appeared during a test rather than between tests.

How Playwright Handles Dialogs — The Event-Driven Approach

Playwright flips the model entirely. Instead of reacting to alerts after they appear, you register a listener before the action that triggers the dialog. The listener fires automatically — no switching, no blocking, no try-catch.

The basic accept/dismiss/prompt patterns work the same way — page.onDialog() takes a lambda with dialog.accept(), dialog.dismiss(), or dialog.accept("input text"). But the pattern that matters is the default handler for unexpected dialogs:

src/test/java/utils/DialogHandler.java
// Default handler for unexpected dialogs — register once
page.onDialog(dialog -> {
System.out.println("Unexpected dialog: " + dialog.message());
dialog.dismiss();
});

No try-catch, no exception overhead, no defensive coding. You register one listener and every dialog gets handled — expected or unexpected, including the session timeout that destroyed our Selenium suite. The dialog fires, the handler dismisses it, the test continues without interruption.

Side-by-Side: The Difference That Matters

Selenium — Reactive

SeleniumAlertTest.java
public void handleUnexpectedAlert() {
try {
Alert alert = driver.switchTo().alert();
alert.dismiss();
} catch (NoAlertPresentException e) {
// swallow — no alert present
}
// Now do actual test work...
driver.findElement(By.id("search")).click();
}

Playwright — Declarative

PlaywrightDialogTest.java
public void handleUnexpectedDialog(Page page) {
// Register once — handles any dialog that appears
page.onDialog(dialog -> dialog.dismiss());
// Do actual test work immediately
page.locator("#search").click();
}

Selenium forces you to react after the fact. Playwright lets you declare intent ahead of time. In a single-threaded suite, this is a style preference. In an 8-thread parallel suite with session timeouts and async server errors, it’s the difference between a stable run and a cascading failure that takes your entire morning to debug.

Why Playwright’s Model Wins in Parallel Execution

The real advantage isn’t just the event-driven API — it’s how Playwright isolates browser state. Each test gets its own BrowserContext and Page instance. A dialog on one page has zero effect on any other page, even within the same browser process.

src/test/java/base/BaseTest.java
public class BaseTest {
protected Page page;
@BeforeMethod
public void setup() {
BrowserContext context = browser.newContext();
page = context.newPage();
// Scoped to THIS page — can't leak to other tests
page.onDialog(dialog -> {
System.out.println("[" + Thread.currentThread().getName()
+ "] Dialog: " + dialog.message());
dialog.dismiss();
});
}
}

Compare that to the Selenium equivalent, which needs ThreadLocal driver management plus the @AfterMethod alert cleanup hook:

src/test/java/base/SeleniumBaseTest.java
public class SeleniumBaseTest {
private static final ThreadLocal<WebDriver> driver = new ThreadLocal<>();
@BeforeMethod
public void setup() {
driver.set(new ChromeDriver());
}
@AfterMethod(alwaysRun = true)
public void teardown() {
try {
driver.get().switchTo().alert().dismiss();
} catch (NoAlertPresentException e) { /* swallow */ }
driver.get().quit();
driver.remove();
}
}

With Playwright, isolation is structural. With Selenium, isolation is something you bolt on with ThreadLocal and defensive exception handling — and hope nobody forgets the cleanup hook.

Alert-Related Test Failures in Parallel

Before

12-15 cascading failures per run from one missed alert

After

Zero — dialog handlers registered once, scoped per page

When Selenium’s Approach Is Fine

I’m not arguing Selenium is unusable for alert handling. It works well enough when:

  • You’re running tests sequentially. No thread contention, no leaked dialogs across tests.
  • Your app has predictable alerts. You know exactly when dialogs appear and can handle them inline.
  • No session timeouts or async server errors. No surprise dialogs showing up unexpectedly mid-test.
  • Your suite is small. Under 100 tests, the try-catch overhead is negligible — you won’t even notice the 200ms per test.

But the moment you’re running 200+ tests in parallel on an enterprise app with session management, timeout dialogs, and async error modals — Selenium’s reactive model becomes a liability. I’ve seen three different teams hit this exact problem independently. Each one lost at least a day before identifying the root cause.

What I’d Do Differently

If I were starting a new Playwright framework today, I’d register a default dialog handler in the base test class from day one — even if the application doesn’t currently use any alerts or dialogs:

src/test/java/base/BaseTest.java
@BeforeMethod
public void setup() {
page.onDialog(dialog -> {
logger.warn("Unexpected dialog: {}", dialog.message());
dialog.dismiss();
});
}

Six lines. That’s the entire safety net. When an unexpected dialog appears six months from now — after a new feature adds a confirmation step, or after the security team adds a session timeout — your suite handles it gracefully instead of cascading into 15 failures.

If you’ve been dealing with thread safety issues in parallel execution, alert handling is likely one of the contributing factors. And if you’re mid-migration from Selenium, check out how Playwright’s locator model eliminates other Selenium pain points beyond just dialogs.

Your next step depends on where you are. If you’re on Selenium: add the @AfterMethod alert cleanup hook before your next CI run — it’s a band-aid, but it stops the bleeding. If you’re evaluating Playwright: register page.onDialog in your base test class. Six lines, zero maintenance. If you’re planning a migration: start with the dialog handling pattern as a proof-of-concept — it’s low-risk and immediately demonstrates the architectural advantage to stakeholders.

Related Posts

Get weekly QA automation insights

No fluff, just battle-tested strategies from 10+ years in the trenches.

No spam. Unsubscribe anytime.