Хабрахабр

[Перевод] Топ-10 ошибок из 1000+ JavaScript-проектов и рекомендации по их устранению

В компании Rollbar, которая занимается созданием инструментов для работы с ошибками в программах, решили проанализировать базу из более чем 1000 проектов на JavaScript и найти в них ошибки, которые встречаются чаще всего. В результате они сформировали список из 10 наиболее часто встречающихся ошибок, проанализировали причины их появления и рассказали о том, как их исправлять и избегать. Они полагают, что знакомство с этими ошибками поможет JS-разработчикам писать более качественный код.

image

Сегодня мы публикуем перевод их исследования.

Методика анализа

В наши дни данные — это всё, поэтому мы нашли, проанализировали и проранжировали ошибки, которые чаще всего встречаются в JavaScript-проектах. А именно, были собраны сведения об ошибках по каждому проекту, после чего было подсчитано количество ошибок каждого вида. Ошибки группировались по их контрольной сумме, методику вычисления которой можно найти здесь. При таком подходе, если, например, в одном проекте обнаружена некая ошибка, которая после этого найдена где-то ещё, такие ошибки группируют. Это позволяет, после анализа всех участвующих в исследовании проектов, получить краткую сводку по ошибкам, а не нечто вроде огромного лог-файла, с которым неудобно работать.

В ходе исследования особое внимание уделялось наиболее часто встречающимся ошибкам. Для того чтобы такие ошибки отобрать, их ранжировали по количеству проектов разных компаний, в которых они встречаются. Если бы в этот рейтинг входило лишь общее число появлений некоей ошибки, то ошибки, характерные для какого-нибудь очень крупного проекта, но редко встречающиеся в других проектах, исказили бы результаты.

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

Ошибки, которые встречаются в JS-проектах чаще всего

Названия ошибок представляют собой сокращённый вариант сообщения об ошибке, которое выдаёт система. Опора на системные сообщения позволяет легко идентифицировать ошибки при их возникновении. Сейчас мы проанализируем каждую из них, расскажем о том, что их вызывает, и о том, как с ними бороться.

1. Uncaught TypeError: Cannot read property

Если вы пишете программы на JavaScript, то вы, вероятно, встречались с этой ошибкой гораздо чаще, чем вам того хотелось бы. Подобная ошибка возникает, например, в Google Chrome при попытке прочитать свойство или вызвать метод неопределённой переменной, то есть той, которая имеет значение undefined. Увидеть эту ошибку в действии можно с помощью консоли инструментов разработчика Chrome.

Ошибка Cannot read property

Эта ошибка может возникнуть по многим причинам, но чаще всего её вызывает неправильная инициализация состояния при рендеринге элемента пользовательского интерфейса. Взглянем на пример того, как подобное может произойти в реальном приложении. Тут мы используем React, но та же ошибка инициализации характерна для Angular, Vue и для любых других фреймворков.

class Quiz extends Component { componentWillMount() { axios.get('/thedata').then(res => { this.setState({items: res.data}); }); } render() { return ( <ul> {this.state.items.map(item => <li key={item.id}>{item.name}</li> )} </ul> ); }
}

Тут надо обратить внимание на две важные вещи:

  1. В самом начале состояние компонента (то есть — this.state) представлено значением undefined.
  2. При асинхронной загрузке данных компонент будет выведен как минимум один раз до того, как данные будут загружены, вне зависимости от того, будет ли это выполнено в componentWillMount или в componentDidMount. Когда элемент Quiz выводится в первый раз, в this.state.items записано undefined. Это, в свою очередь, означает, что itemList получает элементы, которые так же представлены значением undefined. Как результат, мы видим в консоли следующую ошибку: "Uncaught TypeError: Cannot read property ‘map’ of undefined".

Эту ошибку исправить несложно. Проще всего инициализировать состояние в конструкторе подходящими значениями по умолчанию.

class Quiz extends Component { // Добавляем это: constructor(props) { super(props); // Инициализируем состояние и задаём значения элементов по умолчанию this.state = { items: [] }; } componentWillMount() { axios.get('/thedata').then(res => { this.setState({items: res.data}); }); } render() { return ( <ul> {this.state.items.map(item => <li key={item.id}>{item.name}</li> )} </ul> ); }
}

Код вашего приложения будет выглядеть иначе, но мы надеемся, что теперь вы знаете, как исправить эту ошибку в своём проекте и как избежать её появления. Если то, о чём шла речь, вам не подходит — возможно, вам поможет разбор следующих ошибок.

2. TypeError: ‘undefined’ is not an object (evaluating…

Эта ошибка возникает в браузере Safari при попытке прочесть свойство или вызвать метод неопределённого объекта. Взглянуть на эту ошибку можно с помощью консоли инструментов разработчика Safari. На самом деле, тут перед нами та же самая проблема, которую мы разбирали выше для Chrome, но в Safari она приводит к другому сообщению об ошибке.

Ошибка ‘undefined’ is not an object

Исправлять эту ошибку надо так же, как в предыдущем примере.

3. TypeError: null is not an object (evaluating

Эта ошибка возникает в Safari при попытке обратиться к методу или свойству переменной, представленной значением null. Вот как это выглядит в консоли разработчика Safari.

Ошибка TypeError: null is not an object

Напомним, что в JavaScript null и undefined — это не одно и то же, именно поэтому мы видим разные сообщения об ошибках. Смысл значения undefined, записанного в переменную, говорит о том, что переменной не назначено никакого значения, а null указывает на пустое значение. Для того чтобы убедиться в том, что null не равно undefined, можно сравнить их с использованием оператора строгого равенства:

Сравнение undefined и null с помощью операторов нестрогого и строгого равенства

Одна из причин возникновения подобной ошибки в реальных приложениях заключается в попытке использования элемента DOM в JavaScript до загрузки элемента. Происходит это из-за того, что DOM API возвращает null для ссылок на пустые объекты.

Любой JS-код, который работает с элементами DOM, должен выполняться после создания элементов DOM. Интерпретация JS-кода производится сверху вниз по мере появления его в HTML-документе. Поэтому если тег <script> с программой окажется перед кодом, описывающим элементы DOM, программа будет выполнена в ходе разбора страницы до его завершения. Эта ошибка проявится, если элемент DOM, к которому обращаются из скрипта, не был создан до загрузки этого скрипта.

В следующем примере мы можем исправить проблему, добавив в код прослушиватель событий, который оповестит нас о том, что страница полностью загружена. После срабатывания обработчика события, добавленного с помощью addEventListener, метод init() сможет правильно работать с элементами DOM.

<script> function init() { var myButton = document.getElementById("myButton"); var myTextfield = document.getElementById("myTextfield"); myButton.onclick = function() { var userName = myTextfield.value; } } document.addEventListener('readystatechange', function() { if (document.readyState === "complete") { init(); } });
</script> <form> <input type="text" id="myTextfield" placeholder="Type your name" /> <input type="button" id="myButton" value="Go" />
</form>

4. (unknown): Script error

Эта ошибка возникает в том случае, когда неперехваченная ошибка JavaScript пересекает границы доменов при нарушении политики кросс-доменных ограничений. Например, если ваш JS-код размещён на CDN-ресурсе, в сообщении о любой неперехваченной ошибке (то есть, об ошибке, которая не перехвачена в блоке try-catch и дошла до обработчика window.onerror) будет указано Script error, а не полезная для целей устранения этой ошибки информация. Это — один из браузерных механизмов безопасности, направленный на предотвращение передачи данных между фрагментами кода, источниками которого являются разные домены, и которым в обычных условиях запрещено обмениваться информацией.

Вот последовательность действий, которая поможет увидеть эту ошибку.

1. Отправка заголовка Access-Control-Allow-Origin.

Установка заголовка Access-Control-Allow-Origin в состояние * указывает на то, что к ресурсу можно получить доступ из любого домена.

Знак звёздочки можно, при необходимости, заменить на конкретный домен, например так: Access-Control-Allow-Origin: www.example.com. Однако поддержка нескольких доменов — дело довольно сложное. Такая поддержка может не стоить затраченных на её обеспечение усилий, если вы используете CDN, из-за возможного возникновения проблем с кэшированием. Подробности об этом можно посмотреть здесь.

Вот примеры установки этого заголовка в различных окружениях.

Apache

В папке, из которой будут загружаться ваши JavaScript-файлы, создайте файл .htaccess со следующим содержимым:

Header add Access-Control-Allow-Origin "*"

Nginx

Добавьте директиву add_header к блоку location, который отвечает за обслуживание ваших JS-файлов:

location ~ ^/assets/ { add_header Access-Control-Allow-Origin *;
}

HAProxy

Добавьте следующую настройку к параметрам системы, ответственной за поддержку JS-файлов:

rspadd Access-Control-Allow-Origin:\ *

2. Установите crossorigin="anonymous" в теге <script>.

В вашем HTML-файле для каждого из скриптов, для которого установлен заголовок Access-Control-Allow-Origin, установите crossorigin="anonymous" в теге <script>. Перед добавлением свойства crossorigin к тегу <script> проверьте отправку заголовка для файла скрипта. В Firefox, если атрибут crossorigin присутствует, а заголовок Access-Control-Allow-Origin — нет, скрипт выполнен не будет.

5. TypeError: Object doesn’t support property

Эта ошибка возникает в IE при попытке вызова неопределённого метода. Увидеть эту ошибку можно в консоли разработчика IE.

Ошибка Object doesn’t support property

Эта ошибка эквивалентна ошибке "TypeError: ‘undefined’ is not a function", которая возникает в Chrome. Обращаем ваше внимание на то, что речь идёт об одной и той же логической ошибке, о которой различные браузеры сообщают по-разному.

Это — обычная для IE проблема, возникающая в веб-приложениях, которые используют возможности пространств имён JavaScript. Когда возникает эта ошибка, то в 99.9% случаев её причиной является неспособность IE привязывать методы, расположенные в текущем пространстве имён, к ключевому слову this. Например, предположим, что имеется объект Rollbar с методом isAwesome. Обычно, находясь в пределах этого объекта, метод isAwesome можно вызвать так:

this.isAwesome();

Chrome, Firefox и Opera нормально воспримут такую команду. IE же её не поймёт. Таким образом, лучше всего, при использовании подобных конструкций, всегда предварять имя метода именем объекта (пространства имён), в котором он определён:

Rollbar.isAwesome();

6. TypeError: ‘undefined’ is not a function

Эта ошибка возникает в Chrome при попытке вызова неопределённой функции. Взглянуть на эту ошибку можно в консоли инструментов разработчика Chrome и в аналогичной консоли Firefox.

Ошибка TypeError: ‘undefined’ is not a function

Так как подходы к программированию на JavaScript и шаблоны проектирования постоянно усложняются, наблюдается и соответствующий рост числа ситуаций, в которых, внутри функций обратного вызова и замыканий, появляются области видимости, в которых используются ссылки на собственные методы и свойства с использованием ключевого слова this, что является довольно распространённым источником путаницы и ошибок.

Рассмотрим следующий пример:

function testFunction() { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // что такое "this"? }, 0);
};

Выполнение вышеприведённого кода приведёт к следующей ошибке: "Uncaught TypeError: undefined is not a function." Причина появления этой ошибки заключается в том, что при вызове setTimeout() мы, на самом деле, вызываем window.setTimeout(). Как результат, анонимная функция, которая передаётся setTimeout(), оказывается определена в контексте объекта window, у которого нет метода clearBoard().

Традиционный подход к решению этой проблемы, совместимый со старыми версиями браузеров, заключается в том, чтобы просто сохранить ссылку на this в некоей переменной, к которой потом можно будет обратиться из замыкания. Например, это может выглядеть так:

function testFunction () { this.clearLocalStorage(); var self = this; // сохраним ссылку на 'this' пока оно является тем, чем мы его считаем! this.timer = setTimeout(function(){ self.clearBoard(); }, 0);
};

В более современных браузерах можно использовать метод bind() для передачи необходимой ссылки:

function testFunction () { this.clearLocalStorage(); this.timer = setTimeout(this.reset.bind(this), 0); // осуществляем привязку к 'this'
}; function testFunction(){ this.clearBoard(); //возвращаемся к контексту правильного 'this'!
};

7. Uncaught RangeError: Maximum call stack

У возникновения этой ошибки, например, в Chrome, есть несколько причин. Одна из них — бесконечный вызов рекурсивной функции. Вот как выглядит эта ошибка в консоли разработчика Chrome:

Ошибка Maximum call stack size exceeded

Подобное может произойти и в том случае, когда функции передают значение, находящееся за пределами некоего допустимого диапазона значений. Многие функции принимают лишь числа, находящиеся в определённом диапазоне. Например, функции Number.toExponential(digits) и Number.toFixed(digits) принимают аргумент digits, представленный числом от 0 до 20, а функция Number.toPrecision(digits) принимает числа от 1 до 21. Взглянем на ситуации, в которых вызов этих и некоторых других функций приводит к ошибкам:

var a = new Array(4294967295); //OK
var b = new Array(-1); // ошибка! var num = 2.555555;
document.writeln(num.toExponential(4)); //OK
document.writeln(num.toExponential(-2)); //ошибка! num = 2.9999;
document.writeln(num.toFixed(2)); //OK
document.writeln(num.toFixed(25)); // ошибка! num = 2.3456;
document.writeln(num.toPrecision(1)); //OK
document.writeln(num.toPrecision(22)); // ошибка!

8. TypeError: Cannot read property ‘length’

Эта ошибка возникает в Chrome при попытке прочесть свойство length переменной, в которую записано undefined. Взглянем на эту ошибку в консоли инструментов разработчика Chrome.

Ошибка Cannot read property ‘length’

Обычно, обращаясь к свойству length, узнают длину массивов, но вышеописанная ошибка может возникнуть если массив не инициализирован, или если имя переменной скрыто в области видимости, недоступной из того места, где к этой переменной пытаются обратиться. Для того чтобы лучше понять сущность этой ошибки, рассмотрим следующий пример:

var testArray= ["Test"]; function testFunction(testArray) { for (var i = 0; i < testArray.length; i++) { console.log(testArray[i]); }
} testFunction();

При объявлении функции с параметрами эти параметры становятся для неё локальными переменными. В нашем примере это означает, что даже если в области видимости, окружающей функцию, есть переменная testArray, параметр с таким же именем скроет эту переменную и будет восприниматься как локальная переменная функции.

Для того чтобы решить эту проблему, в нашем случае можно пойти одним из следующих двух путей:

  1. Удаление параметра, заданного при объявлении функции (как видно из примера, мы хотим работать с помощью функции с массивом, который объявлен за её пределами, поэтому тут можно обойтись и без параметра функции):
    var testArray = ["Test"]; /* Предварительное условие: определение testArray за пределами функции */
    function testFunction(/* без параметров */) { for (var i = 0; i < testArray.length; i++) { console.log(testArray[i]); }
    } testFunction();
    
  2. Вызов функции с передачей ей ранее объявленного массива:
    var testArray = ["Test"]; function testFunction(testArray) { for (var i = 0; i < testArray.length; i++) { console.log(testArray[i]); }
    } testFunction(testArray);
    

9. Uncaught TypeError: Cannot set property

Когда мы пытаемся получить доступ к неопределённой переменной, то мы, фактически, работаем со значением типа undefined, а этот тип не поддерживает чтение или запись свойств. В подобном случае приложение выдаст следующую ошибку:

"Uncaught TypeError cannot set property of undefined."

Взглянем на неё в браузере Chrome.

Ошибка Cannot set property

Если объект test не существует, будет выдана ошибка "Uncaught TypeError cannot set property of undefined."

10. ReferenceError: event is not defined

Эта ошибка возникает при попытке получить доступ к неопределённой переменной, или к переменной, которая находится за пределами текущей области видимости. Взглянем на неё в консоли Chrome:

Ошибка ReferenceError: foo is not defined

Если вы сталкиваетесь с этой ошибкой при использовании системы обработки событий, убедитесь, что вы работаете с объектом события, переданным в качестве параметра. Более старые браузеры, вроде IE, предлагают глобальный доступ к событиям, но это не характерно для всех браузеров. Эту ситуацию пытаются исправить библиотеки вроде jQuery. В любом случае рекомендуется использовать именно тот объект события, которые передан в функцию обработки событий.

function myFunction(event) { event = event.which || event.keyCode; if(event.keyCode===13){ alert(event.keyCode); }
}

Итоги

Надеемся, вы узнали из нашего рассказа об ошибках что-нибудь новое, такое, что поможет вам избежать ошибок в будущем, а может быть — уже помогло найти ответ на вопрос, который давно не давал вам покоя.

Уважаемые читатели! С какими JS-ошибками вы сталкивались в продакшне?

Теги
Показать больше

Похожие статьи

Кнопка «Наверх»
Закрыть