Сегодня мы публикуем перевод их исследования.
Методика анализа
В наши дни данные — это всё, поэтому мы нашли, проанализировали и проранжировали ошибки, которые чаще всего встречаются в 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>
);
}
}
Тут надо обратить внимание на две важные вещи:
- В самом начале состояние компонента (то есть —
this.state
) представлено значениемundefined
.
- При асинхронной загрузке данных компонент будет выведен как минимум один раз до того, как данные будут загружены, вне зависимости от того, будет ли это выполнено в
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
, параметр с таким же именем скроет эту переменную и будет восприниматься как локальная переменная функции.Для того чтобы решить эту проблему, в нашем случае можно пойти одним из следующих двух путей:
- Удаление параметра, заданного при объявлении функции (как видно из примера, мы хотим работать с помощью функции с массивом, который объявлен за её пределами, поэтому тут можно обойтись и без параметра функции):
var testArray = ["Test"]; /* Предварительное условие: определение testArray за пределами функции */ function testFunction(/* без параметров */) { for (var i = 0; i < testArray.length; i++) { console.log(testArray[i]); } } testFunction();
- Вызов функции с передачей ей ранее объявленного массива:
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-ошибками вы сталкивались в продакшне?
Комментарии (17)
animhotep
06.02.2018 13:54Сейчас мы проанализируем каждую из них, расскажем о том, что их вызывает, и о том, как с ними бороться.
и как же бороться с ошибкой 9?KodyWiremane
06.02.2018 14:29Вовремя инициализировать свои объекты и проверять перед использованием чужие.
animhotep
06.02.2018 16:08я в таких случаях делаю что-то типа
var test = undefined; if (!test) { test = { value: 0 } } test.value = 0;
думал может есть способы по чищеKodyWiremane
06.02.2018 20:45Ну тут от контекста зависит: если вместо отсутствующего объекта предполагается использовать некий дефолтный объект, то да; а если жизненно важный объект отсутствует там, где ожидается его наличие — то надо как-то сообщать об ошибке, дебажить.
serf
06.02.2018 14:03Можно для начала попробовать просто включить strictNullChecks опцию компиляции в TypeScript.
Juma
06.02.2018 16:53А почему нельзя просто использовать проерку содержимого?
if (value === typeof 'string') { // 'function', 'number', Array.isArray(value) // code }
или просто
if (!value) return;
animhotep
06.02.2018 17:11вобще не приятно, когда падает на Cannot set property '...' of undefined
круто было бы, еслиб котнструкция типа if (!value) return; была в прототипах объектов
KodyWiremane
06.02.2018 21:07-1Если объект такой, что undefined — и хрен с ним, то конечно. А вот собрался человек на свадьбу ехать,
var taxi = getTaxi(); taxi.addPassenger(self);
— а такси не приехало. Undefined. Вот это вотif (!value) return
, его же люди не поймут. Прежде всего — [его] невеста.
ThisMan
07.02.2018 08:21Пример с
React
иstate
в первом пункте не совсем правильный. Ошибкой будет
Cannot read property 'items' of null
так как явно не указан
state
, а по умолчанию он равенnull
extempl
07.02.2018 09:28В более современных браузерах можно использовать метод bind() для передачи необходимой ссылки:
В более современных браузерах — это во всех начиная с 13 года. То есть, традиционный подход как раз с
bind/call/apply
. А подход с кешированиемthis
от незнания того чтоbind
существует.
fidgethard
07.02.2018 11:38var test = {}; if (typeof test !== "undefined") { test.value = 1; console.log(test.value); } else { console.log("wtf is 'test'"); } test = undefined; if (typeof test !== "undefined") { test.value = 1; console.log(test.value); } else { console.log("wtf is 'test'"); }
welcomerooot
За
Access-Control-Allow-Origin *
надо руки отрывать.mayorovp
За рекомендацию всегда ставить там * — да, надо. А за наличие публичного API руки отрывать не обязательно…
Zoberg
Дополню, что можно ставить
<script crossorigin='anonymous'
чтобы видеть тексты ошибок.
justboris
Разработчикам Github, Яндекс карт и многим другим тоже отрывать будете?
vintage
Нужно возвращать содержимое Origin, а не *, чтобы авторизация работала.
justboris
Вот это уже конструктивное предложение, интересно.
А то если всем руки поотрывать, кто работать-то будет?