vitest-dev/vitest · error · Error

[vitest] The provider was closed.

Error message

[vitest] The provider was closed.

What it means

Thrown by PlaywrightBrowserProvider._throwIfClosing when this.closing is true. _throwIfClosing guards createContext, openBrowserPage, openPage, and post-open continuation: once close() has begun, any further operation is rejected, the disposable is closed, and the pages/contexts maps plus the browser handle are cleared.

Source

Thrown at packages/browser-playwright/src/playwright.ts:656

  }

  async openPage(sessionId: string, url: string, options: { parallel: boolean }): Promise<void> {
    debug?.('[%s][%s] creating the browser page for %s', sessionId, this.browserName, url)
    const browserPage = await this.openBrowserPage(sessionId, options)
    debug?.('[%s][%s] browser page is created, opening %s', sessionId, this.browserName, url)
    await browserPage.goto(url, { timeout: 0 })
    await this._throwIfClosing(browserPage)
  }

  private async _throwIfClosing(disposable?: { close: () => Promise<void> }) {
    if (this.closing) {
      debug?.('[%s] provider was closed, cannot perform the action on %s', this.browserName, String(disposable))
      await disposable?.close()
      this.pages.clear()
      this.contexts.clear()
      this.browser = null
      this.browserPromise = null
      throw new Error(`[vitest] The provider was closed.`)
    }
  }

  async getCDPSession(sessionid: string): Promise<CDPSession> {
    const page = this.getPage(sessionid)
    const cdp = await page.context().newCDPSession(page)
    return {
      send: cdp.send.bind(cdp),
      on: cdp.on.bind(cdp),
      off: cdp.off.bind(cdp),
      once: cdp.once.bind(cdp),
    } as any // overloaded CDPSession type is too tricky in monorepo
  }

  async close(): Promise<void> {
    process.off('SIGTERM', this.onSIGTERM)

    debug?.('[%s] closing provider', this.browserName)

View on GitHub (pinned to d568f8ce37)

Solutions

  1. Ensure all browser interactions complete before provider.close()/process exit; move late assertions out of global teardown.
  2. Avoid sharing a provider instance across workers (use per-worker contexts).
  3. Catch the error around optional post-close cleanup and treat it as a no-op if the run is already shutting down.
  4. Gate commands on provider.isOpen() / a custom closing flag before dispatch.

Example fix

// before
const page = provider.getPage(sessionId) // may throw if close() raced
await page.click('#btn')

// after
if (!provider.closing && provider.pages.has(sessionId)) {
  const page = provider.getPage(sessionId)
  await page.click('#btn')
}
Defensive patterns

Strategy: validation

Validate before calling

if (provider.closing) {
  throw new Error('provider is closing; skipping browser op')
}
const page = provider.getPage(sessionId)

Type guard

function providerUsable(p: PlaywrightBrowserProvider): boolean {
  return !p.closing && p.browser !== null
}

Try / catch

try {
  await provider.openPage(id, url, { parallel: false })
} catch (err) {
  if (err instanceof Error && /provider was closed/i.test(err.message)) {
    // run is shutting down; skip optional post-close work
  } else throw err
}

Prevention

When it happens

Trigger: Issuing any command (openPage, getCommandsContext-driven ops, getContext, getCDPSession) after provider.close() has started, or racing openPage against a concurrent close (e.g. process exit / SIGTERM during teardown).

Common situations: Tests that issue browser actions in afterEach while the provider is being torn down, parallel runs where one worker closes the shared context, signal handlers aborting a run, or awaiting page operations that resolve after Vitest initiated shutdown.

Related errors


AI-assisted analysis of vitest-dev/vitest@d568f8ce37 (2026-08-03). Data as JSON: /data/errors/dd742fa71633de0d.json. Report an issue: GitHub.