Ошибка: Page.evaluate: Target page, context or browser has been closed — причины и полное решение

Когда скрипт Playwright или Puppeteer внезапно останавливается с сообщением «Page.evaluate: Target page, context or browser has been closed», это означает, что попытка выполнить JavaScript в контексте страницы произошла уже после того, как страница, контекст или сам браузер были закрыты. Ошибка возникает не из-за сбоя самого метода evaluate, а из-за нарушения жизненного цикла объектов автоматизации.

Чаще всего причина кроется в асинхронном коде: отсутствует await, преждевременное закрытие в блоке finally, навигация, которая уничтожает execution context, или гонка между несколькими операциями, совместно использующими один BrowserContext. Понимание точного момента закрытия позволяет быстро локализовать проблему и сделать тесты стабильными даже в CI с ограниченными ресурсами.

Ниже разобран механизм ошибки, типичные сценарии 2025–2026 годов, методы диагностики и проверенные способы устранения без изменения логики тестов.

Механизм возникновения: почему page.evaluate «видит» закрытый target

Метод page.evaluate() отправляет функцию в браузерный процесс через Chrome DevTools Protocol и ждёт результата. Если к моменту отправки сообщения страница уже закрыта (page.isClosed() === true), контекст уничтожен или браузерный процесс завершился, Playwright выбрасывает TargetClosedError с точным текстом «Target page, context or browser has been closed».

Важно различать три уровня закрытия. Закрытие отдельной страницы (page.close()) делает недействительными только операции с этой страницей. Закрытие BrowserContext автоматически закрывает все страницы, которые ему принадлежат. Закрытие Browser завершает весь процесс Chromium/Firefox/WebKit. Во всех трёх случаях любой незавершённый evaluate, click или waitForSelector получает ту же ошибку.

Особенность evaluate заключается в том, что он работает в изолированном execution context. Если между созданием промиса evaluate и его выполнением происходит навигация (click, goto, reload), контекст уничтожается, и даже если страница формально открыта, evaluate может завершиться с подобной ошибкой или с «Execution context was destroyed».

Типичные триггеры ошибки в реальном коде

Самый распространённый случай — отсутствующий await перед операцией, после которой идёт browser.close() или context.close(). Код выглядит корректным, но промис evaluate «висит», а блок finally уже выполняет закрытие.

Второй частый сценарий — использование Array.forEach с асинхронной функцией. forEach не ждёт промисов, поэтому цикл завершается, контекст закрывается, а evaluate ещё работает. Правильная замена — for…of или Promise.all с корректным ожиданием.

Третий триггер — обработчики событий page.on('response'), page.on('framenavigated') или preNavigationHooks в Crawlee, которые продолжают выполняться после того, как requestHandler уже завершился и закрыл страницу. В таких случаях ошибка появляется недетерминированно, особенно под нагрузкой.

Четвёртый сценарий характерен для CI и Docker: нехватка памяти или заполненный /dev/shm (по умолчанию 64 МБ во многих контейнерах) приводит к принудительному завершению процесса Chromium. Playwright фиксирует это как TargetClosedError без предварительного graceful close.

Диагностика: как точно определить, какой объект закрылся

Первым шагом добавьте проверку состояния непосредственно перед подозрительным evaluate:

text
if (page.isClosed()) {
  console.error('Страница уже закрыта');
}
if (context.browser()?.isConnected() === false) {
  console.error('Браузер отключён');
}

Включите трассировку: trace: 'on-first-retry' в playwright.config.ts. После падения откройте trace.zip в Trace Viewer — там видна точная последовательность действий и момент закрытия.

Запустите тест в headed-режиме с page.pause() непосредственно перед evaluate. Это позволяет вручную проверить, жива ли ещё страница. В Docker добавьте --shm-size=1gb или используйте официальный образ Playwright с правильно настроенной shared memory.

По моему опыту использования этого в течение месяца на проекте с 400+ e2e-тестами, именно комбинация isClosed() + trace + headed-режим позволяет за 10–15 минут найти 90 % случаев преждевременного закрытия.

Практические исправления шаг за шагом

1. Всегда ожидайте все асинхронные операции перед закрытием. Используйте try/finally, но закрывайте ресурсы только после await всех промисов:

text
const browser = await chromium.launch();
try {
  const context = await browser.newContext();
  const page = await context.newPage();
  await page.goto('https://example.com');
  const result = await page.evaluate(() => document.title);
  // все операции завершены
} finally {
  await browser.close();
}

2. Заменяйте forEach на for…of:

text
for (const item of items) {
  await page.evaluate((data) => { /* ... */ }, item);
}

3. При работе с навигацией оборачивайте click + evaluate в Promise.all с waitForURL или waitForNavigation.

4. Для event-handlers сохраняйте промисы и await их в requestHandler перед закрытием страницы.

5. В CI уменьшайте количество workers, добавляйте retries: 2 и capture screenshots/traces on failure.

Чек-лист перед закрытием браузера

  • Все вызовы page.evaluate, locator.click, page.waitFor* имеют await.
  • Нет «плавающих» промисов (fire-and-forget).
  • forEach не используется с async-функциями.
  • Обработчики page.on() или preNavigationHooks завершены до context.close().
  • В Docker /dev/shm ≥ 1 ГБ или используется --disable-dev-shm-usage.
  • Перед close проверяется !page.isClosed() и browser.isConnected().
  • Все параллельные операции, которые делят один context, синхронизированы через Promise.allSettled.

После прохождения этого списка большинство случаев ошибки исчезает. Если ошибка остаётся — переходите к анализу crash-логов Chromium.

Распространённые ошибки, которых стоит избегать

  • Закрывать browser в finally, не дождавшись Promise.all операций — это классическая гонка.
  • Использовать один BrowserContext для нескольких независимых параллельных сценариев и закрывать его из одного из них.
  • Игнорировать page.on('close') и page.on('crash') — эти события дают раннее предупреждение.
  • Запускать evaluate сразу после click, который вызывает навигацию, без ожидания нового URL.
  • В serverless (AWS Lambda, Vercel) не ограничивать concurrency и не очищать /tmp — это приводит к OOM и TargetClosedError.

В нашей практике мы сталкивались со случаем, когда ошибка появлялась только на 3–4 % прогонов в CI. Причиной оказался shared context между двумя test.describe-блоками и один из тестов вызывал context.close() в afterAll, пока другой ещё выполнял evaluate. После разделения контекстов и перехода на fixtures проблема исчезла полностью.

Вопросы, которые чаще всего задают разработчики

Почему ошибка появляется только в Firefox, а в Chromium всё стабильно?
Firefox строже относится к lifecycle и быстрее уничтожает контексты при закрытии. Проверьте, нет ли незавершённых промисов именно в Firefox-проекте.

Можно ли игнорировать ошибку через try/catch?
Можно, но это маскирует проблему. Лучше ловить TargetClosedError, логировать stack и состояние isClosed(), а затем решать root cause.

Как отличить graceful close от crash?
При graceful close в логах есть явное context.close() или browser.close(). При crash появляются сообщения об OOM, signal 9 или «Browser closed unexpectedly».

Помогает ли page.evaluateHandle вместо evaluate?
Нет. Если target уже закрыт, любой метод, обращающийся к странице, выбросит ту же ошибку.

Что делать в Crawlee / browser_use, где lifecycle управляет фреймворк?
Передавайте промисы из preNavigationHooks в context и await их в requestHandler перед завершением.

Когда простая правка не поможет и нужен рефакторинг

Если ошибка возникает после того, как вы уже исправили все await и forEach, стоит пересмотреть архитектуру. Признаки: тесты стабильны локально, но падают в CI с разной частотой; ошибка появляется после добавления параллелизма; используется один долгоживущий browser для сотен тестов.

В таких случаях переходите на Playwright Test fixtures — они гарантируют правильный порядок setup/teardown. Разделяйте BrowserContext для независимых сценариев. Ограничивайте maxConcurrency в Crawlee. Добавляйте явную проверку памяти и мониторинг /dev/shm.

Для долговременных скраперов рассмотрите возможность перезапуска браузера каждые N страниц вместо удержания одного процесса часами. Это снижает риск накопления memory leaks и неожиданных TargetClosedError.

Ошибка «Page.evaluate: Target page, context or browser has been closed» — это почти всегда сигнал о нарушении порядка жизненного цикла, а не о баге в Playwright. После системного подхода с диагностикой isClosed, правильным ожиданием промисов и контролем ресурсов в CI она становится редкостью даже в сложных проектах 2026 года.

Марія Лісова

Марія Лісова — авторка блогу Дивосад, досвідчена садівниця з Київщини. З 2010 року веде власний сад і город на 18 сотках. Захоплюється рослинами з дитинства, коли допомагала бабусі на грядках.
Спеціалізується на органічному вирощуванні овочів, ягід та плодових дерев. Віддає перевагу натуральним методам: мульчування, компостування та сидерати. Постійно експериментує з новими сортами та ділиться реальними результатами.
У статтях пише чесно й практично — що спрацювало, а що ні. Мета — допомогти читачам створити красивий і врожайний сад без зайвого стресу. Мама двох дітей, яка мріє передати любов до землі наступному поколінню.

Еще от автора

Предшественники моркови: секреты богатого урожая на украинской грядке

РФ знищила склади Епіцентру, Нової пошти та Rozetka під Києвом

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *