mastra-ai/mastra · error · Error

Cannot launch browser in '${this.status}' state

Error message

Cannot launch browser in '${this.status}' state

What it means

MastraBrowser.launch() enforces a lifecycle state machine. If the browser instance is currently 'closing' or already 'closed', launch() refuses to start a new browser process and throws. A browser can only transition to 'launching' from 'pending' (or be awaited via an existing _launchPromise when already 'launching').

Source

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

  async launch(threadId?: string): Promise<void> {
    // Set current thread if provided, so thread-scoped browsers launch for that thread
    if (threadId !== undefined) {
      this.setCurrentThread(threadId);
    }

    // Already ready
    if (this.status === 'ready') {
      return;
    }

    // Already launching - wait for existing promise
    if (this.status === 'launching' && this._launchPromise) {
      return this._launchPromise;
    }

    // Can't launch if closing/closed
    if (this.status === 'closing' || this.status === 'closed') {
      throw new Error(`Cannot launch browser in '${this.status}' state`);
    }

    this.status = 'launching';
    this.error = undefined;

    this._launchPromise = (async () => {
      try {
        await this.doLaunch();
        this.status = 'ready';

        // Fire onLaunch hook
        if (this.config.onLaunch) {
          await this.config.onLaunch({ browser: this });
        }

        // Notify onBrowserReady callbacks
        this.notifyBrowserReady();
      } catch (err) {

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Recreate the browser instance (new Browser(...)) instead of reusing a closed one.
  2. Call ensureReady() rather than launch() directly — it re-launches browsers that were previously closed.
  3. Serialize lifecycle: await all close()/in-flight close promises before issuing a new launch.
  4. Check this.status (or expose a helper) before calling launch().

Example fix

// before
await browser.close();
await browser.launch(); // throws
// after
await browser.close();
browser = new PlaywrightBrowser({ ...opts }); // or call ensureReady()
await browser.ensureReady();
Defensive patterns

Strategy: try-catch

Validate before calling

// guard before launching
if (browser.status === 'closing' || browser.status === 'closed') {
  throw new Error(`Refusing to launch: browser is ${browser.status}; recreate the instance`);
}
await browser.launch();

Type guard

function canLaunch(b: { status: string }): b is { status: 'pending' | 'launching' } {
  return b.status === 'pending' || b.status === 'launching';
}

Try / catch

try {
  await browser.ensureReady();
} catch (err) {
  if (err instanceof Error && err.message.startsWith("Cannot launch browser in '")) {
    browser = createBrowser(options); // recreate on closed/closing state
    await browser.ensureReady();
  } else throw err;
}

Prevention

When it happens

Trigger: Calling launch() (directly or via ensureReady) on a browser instance on which close()/closeSession has been called, or while a close is still in flight; calling launch() twice concurrently on the same instance after it was closed without resetting status to 'pending'.

Common situations: A long-lived browser object is closed by the app (or by a thread-session timeout) but reused later without recreating the instance; a race where one part of the app closes the browser while an agent request triggers ensureReady; calling connect after disconnecting.

Related errors


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