Interrupt Handlers for Web Automation

oceandoughnut1 pts0 comments

Interrupt handlers for web automation — Orchestra<br>Download free

← BlogInterrupt handlers for web automation<br>July 22, 2026playwrightbrowser-automationconcurrency<br>Every browser-automation script is written as if the web were deterministic:

await page.goto('https://shop.example')<br>await page.locator('#search').fill('gravel bike')<br>await page.locator('#go').click()

And every browser-automation script eventually dies, because the web is not. A cookie banner appears on the second run but not the first. A "subscribe to our newsletter" modal shows up after 20 seconds, but only for EU IPs. The session expires mid-run and a login wall swallows a click. A chat widget slides over the button you're about to press.

The failure mode is always the same: the problem is asynchronous, but the script is linear. The banner doesn't appear at a fixed step — it appears at a fixed time, or on a cookie condition, or on a backend whim. There is no correct place in the sequence to put the if (banner) dismiss() check.

The standard workarounds are all bad:

Sprinkle checks between every step. Now every script is 60% banner-handling boilerplate, and the banner can still appear during a slow step.

Dismiss it once at the start. Works until it reappears.

Retry the whole script on failure. Turns a 200ms problem into a 45-second one, and isn't idempotent if you were halfway through a checkout.

What you actually want is the thing operating systems solved decades ago: an interrupt handler . Register it once, let the main program run, and when the condition fires, handle it and return control.

In Orchestra (a desktop app for building browser automations — the details generalize) that primitive is called a Cue : a step that doesn't run in sequence at all. It arms a watcher, and while the rest of the script executes, the watcher polls. The moment its condition is true, its child steps fire — dismiss the banner, re-login, whatever — and the main flow continues from wherever it was.

What can trigger a cue

Two families, because they need different mechanics:

Polled conditions — checked on a background tick (~150ms):

Selector exists — page.locator(sel).count() > 0

Element visible — matched element is actually displayed (a hidden cookie banner shouldn't fire the handler)

URL matches — regex against page.url()

Page contains text — needle in body innerText

Event conditions — captured by real listeners, buffered, and drained one event per fire:

Network response — URL regex plus a status filter like 401,403,500-599

Page / console error — pageerror or console.error matching a regex

URL changed — fires once per transition, not continuously while the URL matches

The buffered-event design matters. A polled check for "was there a 401?" is racy — the response comes and goes between ticks. So response/error cues attach Playwright listeners that push into a bounded buffer (capped at 100), and each fire consumes exactly one event:

const handler = (r: Response) => {<br>if (!re.test(r.url())) return<br>if (!matchesStatusFilter(r.status(), ranges)) return<br>entry.events.push({ at: Date.now(), kind: 'response', summary: `${r.status()} ${r.url()}` })<br>if (entry.events.length > EVENT_BUFFER_LIMIT) entry.events.shift()<br>page.on('response', handler)

Three 401s while the handler was busy → three fires, in order, no losses (up to the cap). That's a queue semantics decision, and it's the right one for "re-authenticate on every auth failure".

The part nobody tells you: reliability engineering

A naive watcher — if (condition) runHandler() on a timer — has four failure modes, and I hit all of them:

1. Fire loops. The handler runs, the condition is still true (the dismiss button didn't work today), the watcher fires again 150ms later. Forever. Fix: a cooldown (default 500ms) between fires, plus a max fires count (default 1; 0 means unlimited, for banners that genuinely reappear).

2. Stuck handlers. The handler clicks a button that never becomes clickable and now the handler is the thing hanging your run. Fix: a fire timeout (default 30s) races the handler; on timeout the fire is recorded as failed and the main flow keeps going.

3. Zombie watchers. A cue whose handler fails every time will fail every time — a selector rotted, the site changed. Letting it keep firing turns one broken handler into a run-wide tarpit. Fix: a circuit breaker — after N consecutive failed fires (default 3) the cue disables itself and says so in the UI.

4. Handlers that "succeed" without fixing anything. The click ran, no exception — but the banner is still there. From the engine's perspective that's a success; from yours it isn't. Fix: an opt-in verify-resolution mode that re-checks the condition after the handler completes. Still true → the fire counts as a failure (which feeds the circuit breaker).

None of these is exotic. Together they're the difference between a demo feature and one you can leave running overnight.

There's also a reentrancy set (a cue can't fire while it's already...

handler fire page banner fires script

Related Articles