mastra-ai/mastra · error · Error

Browser is ${this.status} and cannot be used

Error message

Browser is ${this.status} and cannot be used

What it means

ensureReady() verifies the browser is usable before delegating to launch(). It recovers from 'pending' and from a completed close (re-launches), but if the status is anything else it cannot handle — typically 'launching' without a tracked launch promise, or another unrecoverable state — it throws with the current status in the message.

Source

Thrown at packages/core/src/browser/browser.ts:861

      // Reset to pending to allow re-launch after close
      if (this.status === 'closed') {
        this.status = 'pending';
      }
      await this.launch();
      return;
    }
    if (this.status === 'launching') {
      await this._launchPromise;
      return;
    }
    if (this.status === 'closing') {
      // Wait for close to complete, then re-launch
      await this._closePromise;
      this.status = 'pending';
      await this.launch();
      return;
    }
    throw new Error(`Browser is ${this.status} and cannot be used`);
  }

  /**
   * Check if the browser is still alive.
   * Override in subclass to detect externally closed browsers.
   * @returns true if browser is alive, false if it was externally closed
   */
  protected async checkBrowserAlive(): Promise<boolean> {
    // Default implementation assumes browser is alive if status is ready
    return true;
  }

  /**
   * Check if the browser is currently running.
   * @param _threadId - Thread identifier (for thread-scoped browsers)
   */
  isBrowserRunning(_threadId?: string): boolean {
    return this.status === 'ready';

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Log/inspect this.status at the throw site and reset it to 'pending' before retrying.
  2. Avoid manually mutating this.status in provider subclasses; use the base-class lifecycle methods.
  3. Recreate the browser instance if its state is unrecoverable.
  4. Add lifecycle locking so only launch()/close() change status.

Example fix

// before (custom provider)
this.status = 'launching'; // manual mutation, promise not tracked
// after
await this.launch(); // base class sets status and tracks _launchPromise
Defensive patterns

Strategy: retry

Validate before calling

// only proceed on known-good states
const usable = ['pending', 'ready', 'closed'];
if (!usable.includes(browser.status)) {
  throw new Error(`Unexpected browser status ${browser.status}; aborting operation`);
}
await browser.ensureReady();

Type guard

function isUsableStatus(s: string): boolean {
  return ['pending', 'ready', 'closed'].includes(s);
}

Try / catch

try {
  await browser.ensureReady();
} catch (err) {
  if (err instanceof Error && /^Browser is .+ and cannot be used$/.test(err.message)) {
    // state machine corrupted: recreate instead of retrying same instance
    browser = createBrowser(options);
    await browser.ensureReady();
  } else throw err;
}

Prevention

When it happens

Trigger: Calling an operation that goes through ensureReady() while the browser is in an intermediate/unexpected status (e.g. status set to 'launching' by another code path, or a custom provider subclass mutating this.status manually and leaving it in a non-standard state).

Common situations: Custom provider subclasses that manage this.status themselves and leave stale values; concurrent lifecycle calls where status was set but the launch promise was cleared; corrupted state after a partially failed close/re-launch cycle.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/4862b808c2c4acfa. Report an issue: GitHub.