Коли скрипт 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 року.