microsoft/playwright · error · JavaScriptErrorInEvaluate

JSHandle is disposed!

Error message

JSHandle is disposed!

What it means

Thrown as a JavaScriptErrorInEvaluate by evaluateExpression() when serializing a call argument that is a JSHandle with an _objectId (i.e., a remote object reference) but whose _disposed flag is true. This means the handle was previously disposed (via handle.dispose() or context/page navigation) but was still passed as an argument to an evaluate call.

Source

Thrown at packages/playwright-core/src/server/javascript.ts:267

export async function evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
  return evaluateExpression(context, String(pageFunction), { returnByValue, isFunction: typeof pageFunction === 'function' }, ...args);
}

export async function evaluateExpression(context: ExecutionContext, expression: string, options: { returnByValue?: boolean, isFunction?: boolean }, ...args: any[]): Promise<any> {
  expression = normalizeEvaluationExpression(expression, options.isFunction);
  const handles: (Promise<JSHandle>)[] = [];
  const toDispose: Promise<JSHandle>[] = [];
  const pushHandle = (handle: Promise<JSHandle>): number => {
    handles.push(handle);
    return handles.length - 1;
  };

  args = args.map(arg => serializeAsCallArgument(arg, handle => {
    if (handle instanceof JSHandle) {
      if (!handle._objectId)
        return { fallThrough: handle._value };
      if (handle._disposed)
        throw new JavaScriptErrorInEvaluate('JSHandle is disposed!');
      const adopted = context.adoptIfNeeded(handle);
      if (adopted === null)
        return { h: pushHandle(Promise.resolve(handle)) };
      toDispose.push(adopted);
      return { h: pushHandle(adopted) };
    }
    return { fallThrough: handle };
  }));

  const utilityScriptObjects: JSHandle[] = [];
  for (const handle of await Promise.all(handles)) {
    if (handle._context !== context)
      throw new JavaScriptErrorInEvaluate('JSHandles can be evaluated only in the context they were created!');
    utilityScriptObjects.push(handle);
  }

  // See UtilityScript for arguments.
  const utilityScriptValues = [options.isFunction, options.returnByValue, expression, args.length, ...args];

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Do not pass disposed handles to evaluate. Re-query the element or re-create the handle before use.
  2. Avoid storing JSHandles across navigation boundaries; re-query after page.goto() or click-induced navigation.
  3. Remove explicit dispose() calls if the handle is still needed downstream — Playwright GCs handles automatically.
  4. Use page.evaluate() with serializable arguments instead of JSHandles when possible.

Example fix

// before
const handle = await page.evaluateHandle('document');
await handle.dispose();
await page.evaluate(h => h.title, handle); // throws [334]

// after
const handle = await page.evaluateHandle('document');
await page.evaluate(h => h.title, handle);
await handle.dispose(); // dispose after last use
Defensive patterns

Strategy: validation

Validate before calling

// Check handle before use
if (handle._disposed) // or track disposal manually
  throw new Error('Handle already disposed');
// Better: re-query the handle
const freshHandle = await page.evaluateHandle('document');

Type guard

function isHandleAlive(handle: import('@playwright/test').JSHandle): boolean {
  // There is no public isDisposed; track disposal via try/catch on jsonValue()
  return true; // best practice: re-query instead of checking
}

Try / catch

try {
  await page.evaluate(fn, handle);
} catch (e) {
  if (e.message === 'JSHandle is disposed!') {
    // Re-query the element/handle and retry
    const fresh = await page.locator(selector).elementHandle();
    return page.evaluate(fn, fresh);
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing a JSHandle to page.evaluate(), element.evaluate(), or similar after calling handle.dispose() on it. Also occurs when the handle's execution context was destroyed by a navigation and the handle was implicitly disposed. The serialization callback checks _disposed before attempting to use the handle.

Common situations: Storing a JSHandle in a variable, disposing it, then later passing it to another evaluate call. Navigation invalidates handles created before the navigation. Using a handle from a previous test step after the page navigated. Explicitly calling dispose() too early in a test helper.

Related errors


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