VLearnVibium

The Complete Vibium Debugging Guide

The complete Vibium debugging guide — verbose logs, actionability checks, screenshots, tracing, and zombie-process fixes to find why a script fails fast.

By Pramod Dutta··14 min read·Verified with Vibium 26.2
▶ Animated overview · made with Remotion

To debug a Vibium script, reproduce the failure with verbose logging (vibium -v), then isolate the cause with three built-in tools: check-actionable tells you which actionability condition is blocking a click or type, a screenshot at the failure point shows the real page state, and a trace records the whole run so you can replay it frame by frame. Vibium is AI-native browser automation on WebDriver BiDi, shipped as a single Go binary that auto-downloads Chrome. Because it auto-waits on actionability before every action, most "bugs" are not Vibium failing — they are a covered element, a wrong selector, a timing assumption, or a leftover zombie process holding a port. Created by Jason Huggins, co-creator of Selenium and Appium, Vibium exposes verbose BiDi logs, an actionability inspector, screenshots, and full session tracing so you can turn a vague "it doesn't work" into a precise, fixable cause. This guide walks the exact pipeline, JavaScript-first with Python alongside.

What is the fastest way to debug a Vibium failure?

The fastest debugging path is a fixed pipeline: observe, isolate, inspect, then replay. Rather than guessing, you run the failing command with more visibility, narrow the failure to one element or step, inspect why that step fails, and — for intermittent bugs — record a trace you can scrub. Each stage answers a different question, so you rarely need all four, but running them in order stops you from changing code blindly.

The diagram maps the loop this guide follows. Verbose logs surface where a command stalls; isolating the step tells you which action fails; check-actionable plus a screenshot tell you why; and a trace captures the run when the failure only shows up sometimes. The final step matters most: Vibium's auto-wait means the real fix is almost always removing a blocker or fixing a selector, not adding retries or sleep().

How do I turn on verbose logging in Vibium?

Add -v or --verbose to any vibium CLI command to print the BiDi protocol messages, timing, and internal state behind an action. This is the single most useful first move, because it shows exactly which command was sent to the browser and what came back — so you can see whether Vibium stalled while waiting for an element, got an error from Chrome, or never sent the command at all.

# See the BiDi traffic and timing for a single navigation
vibium go https://example.com -v

For anything longer than one command, pipe the output to a file so you can search it after the run:

# Capture everything (stdout + stderr) to a log you can grep
vibium go https://example.com -v 2>&1 | tee bidi.log

Then search bidi.log for the arrows that mark sent (->) and received (<-) BiDi messages. A request with no matching response is where the session hung; an error frame tells you what Chrome rejected. On Windows, you can also set the debug environment variable before running — set VIBIUM_DEBUG=1 in CMD, or $env:VIBIUM_DEBUG=1 in PowerShell — to get the same detail.

How do I isolate which step is failing?

Isolating the failing step means shrinking a long script down to the smallest snippet that still reproduces the error. When a 40-line automation "doesn't work," the actual failure is usually one line — and the fastest way to find it is to make the browser visible, slow down, and add checkpoints so you can watch where the run diverges from what you expect.

Start by running non-headless so you can see the page as Vibium drives it. Vibium launches headed by default, but confirm it explicitly and add console.log checkpoints between actions:

const { browser } = require('vibium/sync')
 
const bro = browser.launch({ headless: false })
const page = bro.page()
 
page.go('https://example.com/app')
console.log('1. navigated')
 
page.find('#open-menu').click()
console.log('2. opened menu')          // if you never see this, step 2 threw
 
page.find('a[data-testid="settings"]').click()
console.log('3. clicked item')
 
bro.close()

Whichever checkpoint fails to print marks the failing line. In Python, the same technique uses print() between calls on the sync client:

from vibium import browser_sync as browser
 
vibe = browser.launch(headless=False)
vibe.go("https://example.com/app")
print("1. navigated")
 
vibe.find("#open-menu").click()
print("2. opened menu")
 
vibe.find(role="menuitem", text="Settings").click()
print("3. clicked item")
 
vibe.quit()

Once you know the line, reproduce just that step from a fresh browser. If the isolated step passes on its own but fails inside the full script, the bug is a missing precondition — an earlier action did not complete, a modal from a previous step is still open, or the page had not settled. That distinction (step-is-wrong vs. state-is-wrong) is the single most useful thing isolation tells you, and it decides whether you fix the selector or fix the ordering.

Why does my Vibium click or type fail silently?

A silent click or type failure almost always means the element is not actionable, and Vibium is correctly refusing to interact with it. Before every action, Vibium runs actionability checks: the element must be visible, stable (not moving), enabled, and it must receive events (a hit-test at its center lands on the element, not an overlay); typing additionally requires the node to be editable. When one check never passes within the wait window, the action fails — so the debugging question is which check is false.

The check-actionable command answers that directly. Point it at a URL and a selector, and it prints each condition:

vibium check-actionable https://example.com "button"
# Checking actionability for selector: button
# ✓ Visible: true
# ✓ Stable: true
# ✓ ReceivesEvents: true
# ✓ Enabled: true
# ✗ Editable: false

Read the failing line as a specific instruction. A false ReceivesEvents means a cookie banner, modal, or sticky header covers your target — dismiss it first. A false Visible means the element is hidden or zero-size. A false Enabled means it is disabled, so wait for the state that enables it. A false Editable on a type action means you targeted a <div> or <span> instead of an <input> or <textarea>. This one command replaces a lot of guesswork.

How does this map to common errors?

Use this table to jump from the symptom you see to the check that is failing and the fix.

SymptomLikely failing checkRoot causeFix
Click "works" but nothing happensReceivesEventsOverlay covers the targetDismiss the banner/modal first, then click
Click times out on an animated elementStableElement is still movingLet auto-wait settle it; target the post-animation state
Button click never firesEnabledButton is disabledWait for the enabling condition (valid form, loaded data)
type() does nothingEditableSelector matches a non-input nodeTarget the real <input>/<textarea>
Element never appears(find never resolves)Wrong selector, iframe, or not rendered yetFix selector, switch frame, or order actions correctly

How do I capture a screenshot at the point of failure?

A screenshot taken exactly when an action fails is the quickest way to tell "wrong selector" apart from "element genuinely absent." Wrap the failing action in a try/catch, save a full-page image, and re-throw so you keep the original error:

const fs = require('fs')
const { browser } = require('vibium/sync')
 
const bro = browser.launch()
const page = bro.page()
page.go('https://example.com')
 
try {
  page.find('#promo').click()
} catch (err) {
  fs.writeFileSync('failure.png', page.screenshot({ fullPage: true }))
  throw err
}
 
bro.close()

The Python equivalent uses the sync client the rest of this site favors:

from vibium import browser_sync as browser
 
vibe = browser.launch()
vibe.go("https://example.com")
 
try:
    vibe.find("#promo").click()
except Exception:
    with open("failure.png", "wb") as f:
        f.write(vibe.screenshot(full_page=True))
    raise
 
vibe.quit()

Open failure.png and look: if the element is on screen but your selector missed it, fix the selector; if it never rendered, fix the timing or the step that reveals it. Either way, a picture removes the ambiguity. See the screenshot command for options like full-page capture and element screenshots.

How do I record and replay a Vibium session with tracing?

For failures that only happen sometimes, record a trace and replay it — this is Vibium's most powerful debugging tool. Tracing lives on the browser context and captures a timeline of screenshots, actions, network requests, and DOM snapshots into a single zip. Start it with screenshots enabled, run your flow, then stop it to a file:

const { browser } = require('vibium')
 
async function main() {
  const bro = await browser.launch()
  const page = await bro.page()
 
  await page.context.tracing.start({ screenshots: true, snapshots: true })
 
  await page.go('https://example.com')
  await page.find('a').click()
 
  await page.context.tracing.stop({ path: 'trace.zip' })
  await bro.close()
}
 
main()

Open trace.zip at trace.vibium.dev and you get a scrubber over a screenshot filmstrip, every action marked on the timeline, a network waterfall, and DOM snapshots you can inspect at capture time. For a flaky click, step to the frame just before it and you will usually see the overlay or the half-loaded state that caused it.

How do I label and slice a trace?

Two features make long traces readable. Group related actions so they appear as named spans, and enable raw BiDi capture when you need protocol-level detail:

await page.context.tracing.start({ screenshots: true, bidi: true })
 
await page.go('https://example.com')
 
await page.context.tracing.startGroup('login')
await page.find('#username').fill('alice')
await page.find('#password').fill('secret')
await page.context.tracing.stopGroup()
 
await page.context.tracing.stop({ path: 'trace.zip' })

The bidi: true option records the raw protocol commands (like input.performActions) inside the trace — larger files, but invaluable for low-level issues. You can also drive tracing from the CLI in daemon mode with vibium trace start --screenshots --snapshots and vibium trace stop -o trace.zip.

Trace vs. screenshot vs. verbose logs — which do I reach for?

ToolBest forCostOutput
-v verbose logsSeeing where a single command stalls or errorsFree, instantBiDi message log
Screenshot on failure"Is the element there or not?"One line of codeA PNG of the failure
check-actionable"Which actionability check blocks this?"One CLI callPer-condition pass/fail
TraceIntermittent/flaky bugs, full-run contextSlightly larger runsScrubbable timeline zip

Reach for verbose logs and check-actionable first because they are cheap; escalate to a trace when a bug refuses to reproduce on demand.

Why can't Vibium connect to or launch the browser?

Connection and launch failures are almost always environmental, not code bugs — a stale process, a busy port, or a missing Chrome download. If you see a "failed to connect" error, a previous run's chromedriver likely died while something still expects it, or the port it used is now taken.

Work through it in order:

# 1. Is a chromedriver still running?
ps aux | grep chromedriver
 
# 2. Is its port already in use?
lsof -i :9515
 
# 3. Kill leftover Chrome for Testing + chromedriver, then retry
pkill -f "Chrome for Testing"; pkill -f chromedriver

If Chrome itself will not start, confirm the managed download and reinstall it:

# Show where Vibium put Chrome for Testing and the binary
vibium paths
 
# Re-download Chrome for Testing if it is missing or corrupt
vibium install

On macOS you may need to allow Chrome for Testing under System Settings → Privacy & Security the first time. If you are hitting install or download problems specifically, see Fix: Chrome download fails and Fix: install fails.

How do I debug page-load and network problems?

When an element is genuinely absent rather than merely covered, the cause is often upstream: a failed request, a slow API, or content that renders only after data arrives. The tell-tale sign is that check-actionable reports the element as not found at all — there is nothing to be visible or stable, because the page never produced it. To confirm, look at what the page actually loaded.

The most direct check is to capture the browser's own console output, which surfaces JavaScript errors and failed fetches that break rendering. Vibium lets you subscribe to console messages before you navigate, so you collect everything the page logs during the run:

from vibium import browser_sync as browser
 
vibe = browser.launch()
 
logs = []
vibe.on_console(lambda msg: logs.append((msg.type, msg.text)))
 
vibe.go("https://example.com/app")
 
# A red console error here often explains a missing element
for kind, text in logs:
    if kind == "error":
        print("PAGE ERROR:", text)
 
vibe.quit()

An uncaught error in that list frequently explains the "not found" element — the component threw before it rendered. Pair this with a full-page screenshot to see the visible fallback state. Because listener method names and network-inspection APIs can differ by client and version, confirm the exact signatures against the official reference at vibium.com; the capture console logs and monitor network requests guides show the current, verified patterns.

For content that appears only after a request completes, do not paper over it with sleep(). Let find() auto-wait for the element that the response produces — it polls until the node exists, which naturally covers the network delay. A screenshot on failure plus the console log together tell you whether you are waiting on slow data (retry with a longer wait) or a broken request (fix the environment or the URL).

How do I debug a Vibium MCP or AI-agent run?

Vibium ships a built-in MCP server, so an AI agent like Claude Code can drive the browser directly — and debugging those runs adds one question on top of everything above: is the failure in the browser, or in the tool call the agent made? The browser-side tools are identical (verbose logs, check-actionable, screenshots, traces); what changes is that you also want to see the instructions the agent issued.

The most useful habit is to keep a trace running for agent sessions. Because the agent's actions flow through the same command layer, they show up on the trace timeline exactly like scripted ones, so you can replay what the model actually did rather than what you assume it did:

const { browser } = require('vibium')
 
async function main() {
  const bro = await browser.launch()
  const page = await bro.page()
 
  // Record the whole agent session so every tool-driven action is replayable
  await page.context.tracing.start({ screenshots: true, snapshots: true })
 
  // ... agent drives the page via the MCP server ...
 
  await page.context.tracing.stop({ path: 'agent-run.zip' })
  await bro.close()
}
 
main()

When an agent run fails, open agent-run.zip and look for the frame where the agent's target diverged from the page — a click on a stale selector, a navigation to the wrong URL, or an action fired before the page settled. If the MCP server itself will not connect to your AI client, that is a setup issue rather than a browser bug; see set up Vibium MCP in Claude Code and Fix: MCP not connecting for the connection checklist.

How do I fix zombie Chrome processes and hanging scripts?

Zombie processes appear when a script is force-killed instead of shutting down cleanly, leaving Chrome for Testing and chromedriver running and holding ports. The durable fix is to always let Vibium shut down gracefully — call quit() (Python) or close() (JS) so it can clean up its child processes — rather than pressing Ctrl-C on a runaway script.

When zombies already exist, clear them before the next run:

# macOS / Linux: kill leftover Chrome for Testing + chromedriver
pkill -f "Chrome for Testing"
pkill -f chromedriver

If a script hangs instead of erroring, combine the tools above: run it with -v to see which BiDi command never returns, and check for zombies in parallel, since a stuck prior process is a common cause. A hang that always stops at the same command usually points to an element that never becomes actionable — run check-actionable on that selector to confirm.

What are the best habits for debuggable Vibium scripts?

The cheapest debugging is the bug you never have to chase, and a few habits get you there:

  • Let auto-wait work — remove sleep() calls; Vibium's actionability wait is faster and more reliable. See flaky clicks.
  • Use semantic selectors — target by role, text, label, or testid so locators survive redesigns. See best practices: page object model.
  • Fail fast on optional elements — lower the timeout on elements that may not exist so a missing node errors quickly instead of stalling.
  • Screenshot on every failure — a try/catch that saves a PNG turns "it broke" into a picture.
  • Keep a trace for flaky flows — record login and checkout paths so intermittent failures are replayable.
  • Always call quit()/close() — graceful shutdown prevents the zombie processes that cause the next run's connection errors.

When an API detail is unclear for your Vibium version, check the official docs at vibium.com rather than guessing — the surface is small, but options like timeouts and screenshot flags are worth confirming against the reference.

Next steps

Frequently asked questions

How do I debug a Vibium script?

Reproduce the failure with verbose logging using vibium -v, then narrow it down: check actionability with vibium check-actionable to see which condition fails, capture a screenshot at the failure point, and record a trace to replay the run. Most failures are a covered element, wrong selector, or timing, not a Vibium bug.

How do I see what Vibium is doing under the hood?

Add -v or --verbose to any vibium command to print the BiDi protocol messages, timing, and internal state. Pipe it to a file with vibium go URL -v 2>&1 | tee bidi.log, then search for arrows marking sent and received messages. This shows exactly where a command stalls or errors.

Why does my Vibium click or type fail silently?

A silent failure usually means the element is not actionable. Run vibium check-actionable URL 'selector' to see which of the visible, stable, enabled, receives-events, or editable checks is false. A false receives-events check means an overlay covers the element; a false editable check means you targeted a non-input node.

How do I fix zombie Chrome processes left by Vibium?

If you kill a script mid-run, Chrome for Testing and chromedriver processes can linger and hold ports. On macOS and Linux, find and kill them with pkill, or use the project's cleanup target. Vibium also cleans up on a graceful shutdown, so always let scripts call quit() or close() rather than force-killing them.

How do I record and replay a Vibium session?

Use tracing: call context.tracing.start with screenshots enabled, run your actions, then context.tracing.stop with a path to save a trace.zip. Open the zip at trace.vibium.dev to scrub a screenshot filmstrip, inspect every action, view the network waterfall, and see DOM snapshots — ideal for debugging flaky or intermittent runs.

Why can't Vibium connect to the browser?

A connection-refused error usually means a stale chromedriver is gone or a port is already taken. Check whether chromedriver is running and whether its port is in use, kill any zombie Chrome and chromedriver processes, then retry. If Chrome itself will not launch, verify the download with vibium paths and reinstall with vibium install.

Vibium is created by Jason Huggins. This is an independent tutorial — see the official Vibium site and GitHub repo for canonical docs.

Related guides