Когда скрипт 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:
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 всех промисов:
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:
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 года.