---
title: "Ошибки и отладка chromedp"
description: "Ошибки chromedp и отладка: типовые panic, .Headless-режим с GUI, логи DevTools. Дедлайны, no node set, tab crashed — таблица «ошибка, причина, лечение»."
course: "Chromedp в Go: автоматизация Chrome без Selenium"
url: "https://golangmaster.ru/courses/chromedp/lessons/22-oshibki-otladka/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Ошибки и отладка chromedp

*Курс «Chromedp в Go: автоматизация Chrome без Selenium», урок 22 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/chromedp/lessons/22-oshibki-otladka/*

Ошибки chromedp узнаваемы, как отпечаток: один и тот же набор сообщений проходит через чужие issue на GitHub и через ваши ночные логи — и чем раньше вы выучите их в лицо, тем быстрее чините обходы. chromedp отладка тоже имеет свою технику: программа пишет в лог до и после каждого шага, при падении оставляет скриншот и дамп HTML, а подозрительные случаи разбираются в режиме с окном, где браузер видно глазами. Половина материала уже встречалась по пути: дедлайны из пятнадцатого урока, JS-рендер из одиннадцатого, сотни вкладок из девятнадцатого. Здесь всё собирается в одну дисциплину: типовые ошибки, их причины и лечения, обёртка с логированием шагов и таблица, которую стоит держать под рукой, когда парсер падает в два часа ночи.

Ошибка — не повод переписывать парсер, а повод прочитать её текст целиком. Половина материала этого урока уже встречалась по пути: дедлайны из пятнадцатого, рендер из одиннадцатого, сотни вкладок из девятнадцатого — но там они были техникой, здесь они становятся диагнозами: что именно сломалось, почему и что делать в первую минуту. Ценность дисциплины отладки измеряется ночью, когда обход молча встал: у кого-то в терминале одна строка «panic» и ноль версий, а у вас — лог шагов с временами, дамп HTML и скриншот того, что видел браузер в момент сбоя. Ниже — четыре семейства ошибок с причинами и лечениями, обёртка с логированием шагов, которую стоит вставить в парсер сегодня, и таблица для быстрых разборов.

### Ошибки chromedp: четыре частых гостя

Реальные сбои браузерных парсеров на Go сводятся к четырём семействам, и в логе они выглядят узнаваемо. Первое — context deadline exceeded: дедлайн истёк, страница не успела загрузиться или сайт медленный. Второе — «no node set» или пустой результат query: селектор не нашёл узел, чаще всего потому, что страница рендерится JavaScript'ом позже, чем код пытается читать. Третье — connection refused: Chrome не поднялся или порт отладки занят, программы вообще не с чем разговаривать. Четвёртое — tab crashed и context canceled без вашей вины: вкладка или весь браузер погибли, обычно от памяти при сотнях вкладок. Четыре семейства — не потому, что chromedp беден на ошибки, а потому, что архитектура проста: процесс Chrome, DevTools-протокол поверх вебсокета, контекст Go — и ломаться может только одно из трёх звеньев. Разбор всегда идёт сверху вниз: сначала текст ошибки, потом звено, потом причина — и только после этого код. Дальше — каждое семейство отдельно, с причиной и лечением, а в конце урока таблица для быстрых разборов.

### ctx deadline exceeded: дедлайн истёк

context deadline exceeded — самый частый гость, и почти всегда это не баг библиотеки, а арифметика из пятнадцатого урока: дедлайн шага меньше реального времени страницы. Медленный сайт, тяжёлая страница, сеть на другом краю страны — действие честно не уложилось в потолок. Классификация проста: перишаговый дедлайн истёк — ретрай с бэкоффом и свежим контекстом часто проходит; дедлайн сгорел на первом Run, который аллоцирует браузер, — погибли все последующие Run, и это уже авария конфигурации, а не сеть. Проверяйте причину через errors.Is(err, context.DeadlineExceeded), а не сравнением строк: обёртки и ретраи наворачивают слои, и только errors.Is видит сквозь них. Отдельный случай — context canceled при живом браузере и вашей невиновности: контекст отменила сама библиотека, потеряв соединение с браузером, — ретрай в мёртвый контекст бессмыслен, нужен новый NewContext от аллокатора. И помните про отсчёт: дедлайн стартует от вызова WithTimeout, а не от начала Run, — минуты между созданием контекста и Run съедаются молча. Отдельно тренируйте привычку записывать затраченное время каждого шага: потолок, выведенный из замеров, лечит таймауты навсегда, а угаданный — воспроизводит их еженедельно.

### Дамп HTML и скриншот при падении шага

```go
if err := chromedp.Run(stepCtx,
        chromedp.Navigate(url),
        chromedp.WaitVisible(".list"),
); err != nil {
        // что видит браузер в момент сбоя
        var html string
        if derr := chromedp.Run(ctx,
                chromedp.Evaluate(
                        `document.documentElement.outerHTML`, &html),
        ); derr == nil && html != "" {
                _ = os.WriteFile("dump.html", []byte(html), 0o644)
        }
        var shot []byte
        if serr := chromedp.Run(ctx,
                chromedp.FullScreenshot(&shot, 90),
        ); serr == nil {
                _ = os.WriteFile("dump.png", shot, 0o644)
        }
        log.Printf("%s: %v (html=%d, png=%d)",
                url, err, len(html), len(shot))
}
```

После падения шага дедлайна браузер жив под родительским контекстом — этим пользуются: тем же Run выгружают outerHTML и полный скриншот страницы. Дамп сохраняется с внешним именем, чтобы потом открыть его глазами и увидеть, что же отдал сайт: капчу, заглушку, вёрстку без вашего класса. Errors двух Run независимы: сбой дампа не затирает главную причину.

### «No node set» и пустой query: рендер позже кода

Второе семейство ловит гонку «код быстрее страницы». Query-механика умеет ждать: по умолчанию стоит условие NodeReady — узлы должны существовать, и хелпер внутри крутит проверку с миллисекундными паузами до готовности или дедлайна. Поэтому Text по селектору, которого нет вообще, будет молча ждать до конца контекста — и вернёт таймаут, а не ошибку селектора. Но между «узлы появились» и «данные дописаны» проходит время, и при перерисовке SPA узел, который вы уже прочитали, заменяется новым: действие получает пустой или протухший набор узлов — в логах это всплывает как «no node set» или пустая строка без ошибки. Evaluate добавляет третью ловушку: он ничего не дожидается сам, и мгновенный Evaluate по ещё не отрисованному DOM вернёт null или пустой массив, хотя страница через полсекунды отрисуется. Лечение одно и то же: WaitVisible на устойчивый якорь контента вместо мгновенных чтений, poll-предикаты из девятого урока для изменчивых данных, повтор после перерисовки. Это ровно механика [динамического контента](/courses/chromedp/lessons/11-dinamicheskij-kontent/) из одиннадцатого урока — здесь она проявляется как ошибка, там — как техника. Диагностика простая: тот же селектор через пару секунд находит узел — значит, дело во времени, а не в разметке.

> **Живое место: connection refused на втором запуске.** Ночной парсер упал, его перезапустили скриптом — и второй запуск падает с connection refused, хотя код не менялся. Виновник — зомби: первый процесс Chrome остался висеть в памяти с занятым портом отладки, а новый аллокатор не может ни занять порт, ни найти живой браузер. Это лечится гигиеной процессов: ps aux | grep chrome перед стартом, kill зомби, проверка, что временный каталог профиля прошлой сессии убран. Системный запуск через планировщик обязан включать шаг уборки: зависший процесс с одной утечной вкладкой повторится каждую ночь — и ретраи тут бессильны, потому что проблема не в сети, а в окружении запуска.

### chromedp отладка: окно и скриншоты

chromedp отладка начинается с простого шага: выключите невидимость. В DefaultExecAllocator из второго урока прописан headless-режим — флаг Headless в списке опций, — и выключается он добавлением своей опции Flag("headless", false) к срезу аллокатора: браузер запустится окном, и вы увидите всё своими глазами — где кликает программа, что рисует сайт, не вылезла ли капча. Это самый быстрый отладчик из существующих и его недооценивают: пятнадцать секунд наблюдения отвечают на вопросы, на которые лог не ответит и за вечер. Второй инструмент — FullScreenshot(res, quality): полное изображение страницы целиком, включая то, что за пределами вьюпорта, — «что видит браузер» в отличие от того, что видит curl: скрипты исполнены, стиль применён, картинка честная. Третий — дамп HTML после падения из примера выше, открываемый офлайн. У тройки есть общее правило: собирать их надо в момент сбоя, а не после — перезапуск уже изменит состояние сайта, капча переедет, и разбор станет гаданием. Окно с браузером на рабочем столе выглядит несерьёзно рядом с профайлером, но за пятнадцать секунд наблюдения оно отвечает на вопросы, на которые профайлер не ответит никогда.

### Обёртка с логом шагов: до и после

```go
func logged(step string, act chromedp.Action) chromedp.Action {
        return chromedp.ActionFunc(func(ctx context.Context) error {
                start := time.Now()
                log.Printf("-> %s", step)
                if err := act.Do(ctx); err != nil {
                        log.Printf("xx %s: %v (%s)",
                                step, err, time.Since(start))
                        return err
                }
                log.Printf("ok %s (%s)", step, time.Since(start))
                return nil
        })
}

// chromedp.Run(ctx,
//         logged("навигация", chromedp.Navigate(url)),
//         logged("ожидание", chromedp.WaitVisible(".list")),
//         logged("сбор", chromedp.Evaluate(expr, &rows)),
// )
```

ActionFunc — адаптер, позволяющий обычной функции притворяться действием: обёртка получает шаг, пишет его имя до исполнения, ошибку с длительностью при провале и время при успехе. Act.Do(ctx) исполняет вложенное действие в том же контексте — дедлайны и отмена работают без изменений. Лог превращает падение в отчёт: видно последний успешный шаг и затраченные времена.

### Ошибка, причина, лечение

| Ошибка | Причина | Лечение |
| --- | --- | --- |
| context deadline exceeded | дедлайн меньше реального времени страницы; медленный сайт | errors.Is на тип, ретрай с бэкоффом, потолок шага по замерам |
| context canceled без вины | chromedp потерял браузер и сам отменил контекст | не ретраить: новый NewContext от аллокатора |
| no node set / пусто | JS-рендер позже чтения; узел перерисовался | WaitVisible на якорь, poll-предикат, повтор |
| connection refused | Chrome не поднялся; порт отладки занят зомби | уборка процессов, окружение запуска, не сеть |
| tab crashed | OOM браузера: сотни вкладок, тяжёлые страницы | лимит вкладок, pool, меньше вкладок на браузер |

### tab crashed и утечки вкладок: pprof на помощь

Tab crashed — диагноз памяти: вкладка или браузер погибли, когда одной из сотен вкладок девятнадцатого урока не хватило памяти. Chrome падает по-разному — иногда гаснет вкладка, иногда весь процесс, — но для программы это выглядит одинаково: действия возвращают context canceled, а новый Run в том же контексте молчит. Симптом надёжнее причины: обход с пулом из [девятнадцатого урока](/courses/chromedp/lessons/19-parallelnyj-pul/) начал ронять задания через сорок минут — считайте это сигналом OOM, пока не доказано иное. Лечение — снижение потребления: меньше вкладок на браузер, меньше одновременно живых контекстов, обязательно defer cancel() на каждую вкладку — забытый cancel живёт до конца программы и держит вкладку. Для поиска утечек вкладок подходит стандартный арсенал Go: импорт net/http/pprof поднимает профиль — счётчик горутин, растущий от задания к заданию, почти всегда означает контексты без отмены, а heap-профиль показывает, что именно держит память. Снимайте профили на живом обходе, а не после падения: утечка видна в динамике, где две кривые расходятся. И держите лимит вкладок как константу рядом с пулом: растить параллелизм дешевле, чем лечить OOM на проде.

> **Про песочницу: ошибки не отрепетировать без Chrome.** Песочница сайта исполняет тематические программы на чистом Go без браузера — ни tab crashed, ни connection refused, ни дедлайны на настоящем процессе в ней не воспроизвести честно, и это граница честности, а не упущение. Локальный запуск — полноценный полигон: убейте процесс Chrome руками посреди обхода, поднимите два парсера на одном порту, оборвите сеть на шаге — каждая ошибка из таблицы воспроизводится за минуту, и вы увидите настоящий текст ошибки в логе.

### Чек-лист разбора падения

- прочитать текст ошибки целиком, классифицировать errors.Is, не глазами на строку
- deadline exceeded — проверить, где стоит дедлайн и что в нём учитывается
- context canceled при живой программе — умер браузер, восстанавливать контекст
- пустой результат — ожидание на якорь до чтения, Evaluate не ждёт ничего
- connection refused — процессы и порт, не сеть: ps и уборка зомби
- tab crashed — память: лимит вкладок, cancel на каждую, pprof по горутинам
- дамп HTML и скриншот — в момент сбоя, потом окно headless=false и глазами

### Проверка: context canceled без вины

**Вопрос:** Ночной обход вернул context canceled, хотя программа ничего не отменяла. Обход в этом же контексте продолжать?

- Да: canceled значит пауза, следующие Run продолжат обход
- Да, но с ретраем и бэкоффом, как после deadline exceeded
- ✅ Нет: библиотека потеряла браузер и отменила контекст — нужен новый NewContext
- Нет: ошибка фатальна, перезапускать всю программу

Соединение с браузером пропало — chromedp сам отменяет контекст: процесс связан с ним, и отмена гасит всё. Контекст мёртв навсегда, ретрай в него вернёт ту же ошибку. Программа целиком не нужна: новый NewContext от живого аллокатора даёт свежий браузер с теми же флагами — механика из пятнадцатого урока.

### Проверка: Evaluate вернул пусто сразу

**Вопрос:** Evaluate по селектору таблицы вернул пустой массив мгновенно, а тот же Text на том же селекторе висит до дедлайна. Почему разные?

- Evaluate сломан: нужно оборачивать его в WaitReady всегда
- ✅ Evaluate ничего не дожидается, а Text через Query ждёт узлов (NodeReady) до дедлайна
- Text использует другой протокол без ожидания
- Разница только в типе результата: массив против строки

Evaluate исполняет выражение в текущем состоянии DOM без всякого ожидания — пусто вернулось потому, что страница ещё не отрисована. Text — Query-действие: механизм ждёт готовности узлов и выходит по дедлайну, если их нет. Лечение одинаковое: WaitVisible на якорь перед любым чтением.

### Что напишет лог

```go
// обёртка logged из урока:
//   "-> шаг" до, "ok шаг (время)" после,
//   "xx шаг: err (время)" при ошибке
err := chromedp.Run(ctx,
        logged("навигация", chromedp.Navigate(url)),      // ok, 1.2s
        logged("ожидание", chromedp.WaitVisible(".zzz")), // дедлайн 3s
        logged("сбор", chromedp.Evaluate(expr, &rows)),
)
```

**Что выведет программа?**

- Три строки: ok навигация, ok ожидание, ok сбор
- ✅ -> навигация, ok навигация (1.2s), -> ожидание, xx ожидание: deadline exceeded (3s) — сбор не начнётся
- Только ошибку: Run молчит до первого сбоя
- Две ошибки: ожидание и сбор упадут вместе

Run исполняет действия по порядку: навигация успела, ожидание прожгло дедлайн и вернуло ошибку — Run остановился на нём, до «сбора» очередь не дошла. Обёртка напишет обе половины: старт шага и провал с длительностью, поэтому по логу видно и последний успешный шаг, и время до падения.

### Соберите обёртку с логом

Восстановите адаптер ActionFunc и исполнение действия.

```go
func logged(step string, act chromedp.Action) chromedp.Action {
        return chromedp.___(func(ctx context.Context) error {
                start := time.Now()
                log.Printf("-> %s", step)
                if err := act.___(ctx); err != nil {
                        log.Printf("xx %s: %v (%s)",
                                step, err, time.___(start))
                        return err
                }
                return nil
        })
}
```

Подсказка: Фабрика действий называется ActionFunc, вложенное действие исполняет Do, длительность даёт time.Since.

Правильные ответы в пропусках: `ActionFunc`, `Do`, `Since`

### Расставьте разбор падения по шагам

Порядок действий, когда ночной обход упал: от чтения ошибки до воспроизведения глазами.

Правильный порядок строк:

1. `err := chromedp.Run(stepCtx, actions...)`
2. `if errors.Is(err, context.DeadlineExceeded) { ... }`
3. `chromedp.Run(ctx, chromedp.Evaluate(outerHTMLExpr, &html))`
4. `chromedp.Run(ctx, chromedp.FullScreenshot(&shot, 90))`
5. `os.WriteFile("dump.html", []byte(html), 0o644)`
6. `Flag("headless", false) в опциях аллокатора`
7. `перезапуск и наблюдение окном своими глазами`

### Карточки: ошибки и отладка

- **context deadline exceeded** — Дедлайн истёк: страница медленнее потолка. Проверка errors.Is, ретрай с бэкоффом, потолок шага по замерам. Отсчёт — от WithTimeout, а не от начала Run.
- **«No node set» / пусто** — Код быстрее рендера: JS дорисовывает позже или узел перерисовался под действием. Лечение: WaitVisible на якорь, poll-предикат, повтор чтения.
- **connection refused** — Chrome не поднялся или порт отладки занят зомби-процессом прошлой сессии. Чинится уборкой процессов и окружением запуска — сеть тут ни при чём.
- **tab crashed** — OOM: вкладке или браузеру не хватило памяти, обычно при сотнях вкладок. Лимит вкладок на браузер, cancel на каждую, pprof по горутинам для поиска утечек.
- **FullScreenshot** — Полное изображение страницы целиком, включая за пределами вьюпорта: «что видит браузер». Снимается в момент сбоя, пока сайт не сменил состояние.
- **Обёртка logged** — ActionFunc-адаптер: лог имени шага до, ошибки с длительностью при провале, времени при успехе. act.Do(ctx) исполняет вложенное действие в том же контексте.

Что дальше: после разбора сбоев остаётся два шага до эксплуатации. Двадцать четвёртый урок поставит вежливость и закон на службу — [этику скрапинга](/courses/chromedp/lessons/24-etika-zakon/): паузы, robots.txt и границы, за которыми парсер превращается в проблему. Двадцать третий уедет с вами на сервер — [Docker и серверное окружение](/courses/chromedp/lessons/23-docker-server/): там connection refused станет главной ошибкой первых дней. Домашняя проверка — локально: заведите обёртку logged в парсере каталога из предыдущего урока, затем воспроизведите три аварии подряд — убитый Chrome, занятый порт, селектор мимо рендера — и убедитесь, что по одному логу с дампами вы можете рассказать, что случилось, не глядя в код.

## Коротко: что запомнить

- Четыре семейства сбоев: deadline exceeded, no node set, connection refused, tab crashed — по звену на каждый: дедлайн, рендер, процесс, память.
- errors.Is вместо сравнения строк: обёртки наворачивают слои, и только errors.Is видит первопричину.
- context canceled без вашей вины — библиотека потеряла браузер: ретрай бессилен, нужен новый NewContext от аллокатора.
- Text по несуществующему селектору ждёт до дедлайна, Evaluate ничего не ждёт и возвращает пусто мгновенно.
- Лечение гонки рендера одно: WaitVisible на якорь перед чтением, poll-предикат для изменчивых данных.
- Дамп HTML и FullScreenshot собираются в момент сбоя: перезапуск изменит состояние сайта.
- headless=false — Flag("headless", false) поверх DefaultExecAllocator: браузер окном отвечает быстрее любого лога.
- Обёртка на ActionFunc пишет шаг до и после: по логу виден последний успешный шаг и затраченные времена.
- pprof на живом обходе: растущий счётчик горутин — это контексты и вкладки без cancel.
- Разбор всегда сверху вниз: текст ошибки, звено, причина — и только потом код; замеры времени шага лечат таймауты навсегда.

## Домашнее задание

Вооружите парсер каталога из предыдущего урока: обёртка logged на каждом шаге, дамп HTML и FullScreenshot при падении, паузы по-прежнему на месте. Затем устройте аварии по расписанию: убейте Chrome посреди обхода и убедитесь, что по логу видно мёртвый контекст; запустите второй экземпляр на том же порту и прочитайте connection refused; поставьте селектор мимо фактического рендера и посмотрите дамп — на нём должна быть видна страница без вашего класса. Снимите pprof-профиль горутин до и после обхода из тридцати вкладок и объясните разницу. Всё — на локальном Chrome, без внешних сайтов. Финал: вставьте в лог замер времени каждого шага и решите по замерам, какой из дедлайнов в вашем парсере стоит слишком щедро, а какой — впритык.

## Вопросы и ответы по уроку

### Как отличить таймаут своей программы от смерти браузера?

По типу ошибки и по вашей роли: errors.Is(err, context.DeadlineExceeded) — истёк дедлайн, браузер жив, ретрай уместен. errors.Is(err, context.Canceled) при том, что программа сама не вызывала cancel, — контекст отменила библиотека, потеряв браузер: контекст мёртв, восстанавливайте NewContext от аллокатора. Помогает и лог обёртки: таймаут наступает в конце ожидания с длительностью около потолка, смерть браузера — внезапно посреди шага.

### Дамп HTML бесполезен: там одна заглушка без контента. Это плохой дамп?

Нет, это самый информативный дамп: заглушка вместо каталога означает, что сайт отдал антибот-страницу, капчу или просит JavaScript-навигацию — парсер попал не на контент, а на фильтр. Сравните дамп с той же страницей в обычном браузере: если глазами контент есть, а в дампе нет, вы читаете слишком рано или не ту вкладку; если заглушка и у людей — вопрос к источнику трафика и лимитам, а не к коду.

### Нужен ли pprof постоянно или только при аварии?

Достаточно лёгкой формы: счётчик горутин и память в лог раз в несколько минут по runtime.NumGoroutine и runtime.ReadMemStats — это почти бесплатно и сразу видно утечку вкладок и контекстов. Полный pprof-сервер с heap-профилями включайте точечно на время разбора: он пишет из себя память и открывает порт, что на сервере с внешним доступом само по себе риск. Динамика важнее снимка: утечка — это расходящиеся кривые, а не высокое значение.
