microsoft/playwright · critical · Error

Frame was detached

Error message

Frame was detached

What it means

frame.context(world) awaits the world's contextPromise. If the promise resolves to a destroyed marker (not an ExecutionContext) - because the frame was detached/destroyed - it throws the stored destroyedReason, conventionally 'Frame was detached'.

Source

Thrown at packages/playwright-core/src/server/frames.ts:795

  }

  async waitForLoadState(progress: Progress, state: types.LifecycleEvent): Promise<void> {
    const waitUntil = verifyLifecycle('state', state);
    if (!this._firedLifecycleEvents.has(waitUntil))
      await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise;
  }

  async frameElement(progress: Progress): Promise<dom.ElementHandle> {
    return await progress.race(this._page.delegate.getFrameElement(this));
  }

  context(world: types.World): Promise<dom.FrameExecutionContext> {
    if (this._page.delegate.noUtilityWorld?.())
      world = 'main';
    return this._contextData.get(world)!.contextPromise.then(contextOrDestroyedReason => {
      if (contextOrDestroyedReason instanceof js.ExecutionContext)
        return contextOrDestroyedReason;
      throw new Error(contextOrDestroyedReason.destroyedReason);
    });
  }

  mainContext(): Promise<dom.FrameExecutionContext> {
    return this.context('main');
  }

  existingContext(world: types.World): dom.FrameExecutionContext | null {
    if (this._page.delegate.noUtilityWorld?.())
      world = 'main';
    return this._contextData.get(world)?.context || null;
  }

  utilityContext(): Promise<dom.FrameExecutionContext> {
    return this.context('utility');
  }

  async evaluateExpression(progress: Progress, expression: string, options: { isFunction?: boolean, world?: types.World } = {}, arg?: any): Promise<any> {

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Drop stale Frame references; re-query page.frame(...) or page.mainFrame() as needed.
  2. Guard operations by checking the frame still exists in page.frames().
  3. Await the action that removes the iframe only after you are done with the frame.
  4. Listen for the frameDetached event and cancel pending work.

Example fix

// before
const frame = page.frame({ name: 'widget' });
await removeIframe();
await frame.evaluate(() => 1); // throws 'Frame was detached'
// after
const frame = page.frame({ name: 'widget' });
await frame.evaluate(() => 1);
await removeIframe();
Defensive patterns

Strategy: validation

Validate before calling

// Verify the frame is still attached before use
if (!page.frames().includes(frame)) throw new Error('frame detached');
await frame.evaluate(() => 1);

Type guard

function isFrameAlive(page: any, frame: any): boolean {
  return page.frames().includes(frame);
}

Try / catch

try {
  await frame.evaluate(fn);
} catch (e) {
  if (e instanceof Error && /Frame was detached|Target frame/i.test(e.message)) { /* re-acquire */ }
  else throw e;
}

Prevention

When it happens

Trigger: Requesting a frame's execution context after the frame has been detached from the DOM (removed iframe, closed popup window, navigation that destroyed the frame).

Common situations: Holding a Frame reference and using it after the iframe was removed from the page; acting on popup windows after they closed; operations scheduled on a frame that detached between scheduling and execution.

Related errors


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/a1a9ec85d38a6b6e. Report an issue: GitHub.