Каждая загрузка PDF приводила к одному и тому же ReferenceError. Стек вызовов не давал никакой полезной информации, а спецификация, которой следовал код, на бумаге выглядела вполне разумно. В ней говорилось проверить флаг функции и сравнить каждый регион с половиной pageWidth. Она описывала, что должно произойти, но не описывала, откуда должно взяться значение pageWidth, и этого единственного упущения хватило, чтобы обрушить весь конвейер.
Архитектурные документы хорошо объясняют поведение, но часто совершенно не справляются с описанием границ. Предложение вроде «функция проверяет x относительно pageWidth» — это не технический контракт, а повествование, скрывающее зависимость за обычными словами. Когда разработчик читает это предложение и пишет функцию на уровне модуля, которая ссылается на pageWidth по имени, код кажется правильным, так как он соответствует описанию. Затем среда выполнения пытается разрешить имя, не находит ничего в области видимости и выбрасывает ошибку.
Рефакторинг, который должен был быть простым
Я столкнулся именно с такой моделью во время рефакторинга сборки страниц. Спецификация содержала два, казалось бы, четких требования:
- Проверить
FEATURE_LAYOUT. - Сравнить каждый регион с
pageWidth / 2.
Разработчик точно следовал инструкциям. Он извлек вспомогательную функцию на уровне модуля и вставил pageWidth прямо в тело функции, не указав её как параметр. В спецификации не было сказано, что pageWidth должен передаваться через список аргументов. Не было сказано, что функция находится на уровне модуля, где pageWidth уже не виден. Предполагалось, что исполнитель подразумевает контекст выполнения по умолчанию.
Результатом стал ReferenceError при каждой загрузке PDF. Поскольку переменная отсутствовала в области видимости модуля, функция немедленно выбрасывала ошибку. Если бы в спецификации были явно указаны границы функции и её входные данные, разработчик передал бы pageWidth в качестве аргумента, и баг стал бы структурно невозможным. Вместо этого инструкция сработала как ловушка, побуждая исполнителя обратиться к родительской области видимости, которой не существовало.
Четыре значения слова «использует»
Глубинная проблема заключается в том, что у прозы нет системы типов. Когда в спецификации говорится «функция использует X», это предложение двусмысленно как минимум четырьмя способами в современной кодовой базе JavaScript:
- Функция получает X как формальный параметр.
- Функция считывает X из переменной на уровне модуля, объявленной в том же файле.
- Функция замыкается на X из вложенной родительской области видимости.
- Функция извлекает X из переданного ей более крупного объекта.
Каждый из этих вариантов соответствует формулировке спецификации. Каждый из них проходит статический анализ. Но только один из них верен для данной границы, а неверный выбор скрытно переносит допущения через эту границу так, что это не вызывает ошибок при компиляции.
Разработчики обычно выбирают путь наименьшего сопротивления в момент написания кода. Если pageWidth по какой-то причине находится во внешней области видимости, они прочитают её оттуда, вместо того чтобы менять сигнатуру функции. Замыкание скрывает зависимость. Код работает при первом запуске, проходит тесты и отправляется в продакшн. Недели спустя кто-то переносит эту же функцию в другой файл для повторного использования или улучшения читаемости. Родительская область видимости исчезает. Код ломается, и эта поломка выглядит как новая регрессия, хотя первопричиной была исходная скрытая зависимость.
Web Workers стирают улики
Эта проблема становится по-настоящему коварной, как только в архитектуру входят Web Workers. Когда внутри воркера происходит ошибка, браузер удаляет самую важную информацию.
Вот что происходит на самом деле. Внутри воркера необработанное исключение вызывает событие ErrorEvent. Если воркер пересылает эту ошибку в основной поток, типичный паттерн заключается в том, чтобы взять строку message и передать её через границу. Основной поток получает эту строку, создает из неё новый объект Error и логирует или перевыбрасывает его. В DevTools отображается реконструированная ошибка внутри обработчика сообщений основного потока. Оригинальное имя файла, номер строки и стек вызовов отбрасываются. Реальное место сбоя становится невидимым.
Поэтому, когда отсутствие pageWidth вызвало ReferenceError внутри воркера, основной поток сообщил только текст «pageWidth is not defined» в том месте, где обрабатывалось сообщение. Сама функция на уровне модуля находилась в
