- PVSM.RU - https://www.pvsm.ru -
Обработка ошибок в JavaScript — дело рискованное. Если вы верите в закон Мёрфи [1], то прекрасно знаете: если что-то может пойти не так, именно это и случится! В этой статье мы рассмотрим подводные камни и правильные подходы в сфере обработки ошибок в JS. А напоследок поговорим об асинхронном коде и Ajax.
Я считаю, что событийная парадигма JS добавляет языку определённое богатство. Мне нравится представлять браузер в виде машины, управляемой событиями, в том числе и ошибками. По сути, ошибка — это невозникновение какого-то события, хотя кто-то с этим и не согласится. Если такое утверждение кажется вам странным, то пристегните ремни, эта поездка будет для вас необычной.
Все примеры будут рассмотрены применительно к клиентскому JavaScript. В основу повествования легли идеи, озвученные в статье «Исключительная обработка событий в JavaScript [2]». Название можно перефразировать так: «При возникновении исключения JS проверяет наличие обработчика в стеке вызовов». Если вы незнакомы с базовыми понятиями, то рекомендую сначала прочитать ту статью. Здесь же мы будем рассматривать вопрос глубже, не ограничиваясь простыми потребностями в обработке исключений. Так что когда в следующий раз вам опять попадётся блок try...catch
, то вы уже подойдёте к нему с оглядкой.
Использованный для примеров код вы можете скачать с GitHub [3], он представляет собой вот такую страницу:
При нажатии на каждую из кнопок взрывается «бомба», симулирующая исключение TypeError
. Ниже приведено определение этого модуля из модульного теста.
function error() {
var foo = {};
return foo.bar();
}
Сначала функция объявляет пустой объект foo
. Обратите внимание, что нигде нет определения bar()
. Давайте теперь посмотрим, как взорвётся наша бомба при запуске модульного теста.
it('throws a TypeError', function () {
should.throws(target, TypeError);
});
Тест написан в Mocha [4] с помощью тестовых утверждений из should.js [5]. Mocha выступает в качестве исполнителя тестов, а should.js — в качестве библиотеки утверждений. Если вы пока не сталкивались с этими тестовыми API, то можете спокойно их изучить. Исполнение теста начинается с it('description')
, а заканчивается успешным или неуспешным завершением в should
. Прогон можно делать прямо на сервере, без использования браузера. Я рекомендую не пренебрегать тестированием, поскольку оно позволяет доказать ключевые идеи чистого JavaScript.
Итак, error()
сначала определяет пустой объект, а затем пытается обратиться к методу. Но поскольку bar()
внутри объекта не существует, то возникает исключение. И такое может произойти с каждым, если вы используете динамический язык вроде JavaScript!
Рассмотрим пример неправильной обработки ошибки. Я привязал запуск обработчика к нажатию кнопки. Вот как он выглядит в модульных тестах:
function badHandler(fn) {
try {
return fn();
} catch (e) { }
return null;
}
В качестве зависимости обработчик получает коллбэк fn
. Затем эта зависимость вызывается изнутри функции-обработчика. Модульные тесты демонстрируют её использование:
it('returns a value without errors', function() {
var fn = function() {
return 1;
};
var result = target(fn);
result.should.equal(1);
});
it('returns a null with errors', function() {
var fn = function() {
throw Error('random error');
};
var result = target(fn);
should(result).equal(null);
});
Как видите, в случае возникновения проблемы этот странный обработчик возвращает null
. Коллбэк fn()
при этом может указывать либо на нормальный метод, либо на «бомбу». Продолжение истории:
(function (handler, bomb) {
var badButton = document.getElementById('bad');
if (badButton) {
badButton.addEventListener('click', function () {
handler(bomb);
console.log('Imagine, getting promoted for hiding mistakes');
});
}
}(badHandler, error));
Что плохого в получении просто null
? Это оставляет нас в неведении относительно причины ошибки, не даёт никакой полезной информации. Подобный подход — остановка выполнения без внятного уведомления — может быть причиной неверных решений с точки зрения UX, способных приводить к повреждению данных. Можно убить несколько часов на отладку, при этом упустив из виду блок try...catch
. Приведённый выше обработчик просто глотает ошибки в коде и притворяется, что всё в порядке. Такое прокатывает в компаниях, не слишком заботящихся о высоком качестве кода. Но помните, что скрытие ошибок в будущем чревато большими временны́ми потерями на отладку. В многослойном продукте с глубокими стеками вызовов практически невозможно будет найти корень проблемы. Есть ряд ситуаций, когда имеет смысл использовать скрытый блок try...catch
, но в обработке ошибок этого лучше избегать.
Если вы будете применять остановку выполнения без внятного уведомления, то в конце концов вам захочется подойти к обработке ошибок более разумно. И JavaScript позволяет использовать более элегантный подход.
Идём дальше. Теперь пришла пора рассмотреть кривой обработчик ошибок. Здесь мы не будем касаться использования DOM, суть та же, что и в предыдущей части. Кривой обработчик отличается от плохого только способом обработки исключений:
function uglyHandler(fn) {
try {
return fn();
} catch (e) {
throw Error('a new error');
}
}
it('returns a new error with errors', function () {
var fn = function () {
throw new TypeError('type error');
};
should.throws(function () {
target(fn);
}, Error);
});
Если сравнить с плохим обработчиком — стало определённо лучше. Исключение заставляет «всплыть» стек вызовов. Здесь мне нравится то, что ошибки будут отматывать (unwind) стек, а это крайне полезно для отладки. При возникновении исключения интерпретатор отправится вверх по стеку в поисках другого обработчика. Это даёт нам немало возможностей для работы с ошибками на самом верху стека вызовов. Но поскольку речь идёт о кривом обработчике, то изначальная ошибка просто теряется. Приходится возвращаться вниз по стеку, пытаясь найти исходное исключение. Хорошо хоть, что мы знаем о существовании проблемы, выбросившей исключение.
Вреда от кривого обработчика меньше, но код всё равно получается с душком. Давайте посмотрим, есть ли у браузера для этого подходящий туз в рукаве.
Отмотать исключения можно одним способом — поместив try...catch
наверху стека вызовов. Например:
function main(bomb) {
try {
bomb();
} catch (e) {
// Handle all the error things
}
}
Но у нас же браузер управляется событиями, помните? А исключения в JavaScript — такие же полноправные события. Поэтому в данном случае интерпретатор прерывает исполнение текущего контекста и производит отмотку. Мы можем использовать глобальный обработчик событий onerror
, и выглядеть это будет примерно так:
window.addEventListener('error', function (e) {
var error = e.error;
console.log(error);
});
Этот обработчик может выловить ошибку в любом исполняемом контексте. То есть любая ошибка может стать причиной события-ошибки (Error event). Нюанс здесь в том, что вся обработка ошибок локализуется в одном месте в коде — в обработчике событий. Как и в случае с любыми другими событиями, вы можете создавать цепочки обработчиков для работы со специфическими ошибками. И если вы придерживаетесь принципов SOLID [6], то сможете задавать каждому обработчику ошибок свою специализацию. Регистрировать обработчики можно в любое время, интерпретатор будет прогонять столько обработчиков в цикле, сколько нужно. При этом вы сможете избавить свою кодовую базу от блоков try...catch
, что только пойдёт на пользу при отладке. То есть суть в том, чтобы подходить к обработке ошибок в JS так же, как к обработке событий.
Теперь, когда мы можем отматывать стек с помощью глобальных обработчиков, что мы будем делать с этим сокровищем?
Стек вызовов — невероятно полезный инструмент для решения проблем. Не в последнюю очередь потому, что браузер предоставляет информацию как есть, «из коробки». Конечно, свойство стека [7] в объекте ошибки не является стандартным, но зато консистентно доступно в самых свежих версиях браузеров.
Это позволяет нам делать такие классные вещи, как логгирование на сервер:
window.addEventListener('error', function (e) {
var stack = e.error.stack;
var message = e.error.toString();
if (stack) {
message += 'n' + stack;
}
var xhr = new XMLHttpRequest();
xhr.open('POST', '/log', true);
xhr.send(message);
});
Возможно, в приведённом коде это не бросается в глаза, но такой обработчик событий будет работать параллельно с приведённым выше. Поскольку каждый обработчик выполняет какую-то одну задачу, то при написании кода мы можем придерживаться принципа DRY [8].
Мне нравится, как эти сообщения вылавливаются на сервере.
Это скриншот сообщения от Firefox Developer Edition 46. Обратите внимание, что благодаря правильной обработке ошибок здесь нет ничего лишнего, всё кратко и по существу. И не нужно прятать ошибки! Достаточно взглянуть на сообщение, и сразу становится понятно, кто и где кинул исключение. Такая прозрачность крайне полезна при отладке кода фронтенда. Подобные сообщения можно складировать в персистентном хранилище для будущего анализа, чтобы лучше понять, в каких ситуациях возникают ошибки. В общем, не нужно недооценивать возможности стека вызовов, в том числе для нужд отладки.
JavaScript извлекает асинхронный код из текущего исполняемого контекста. Это означает, что с выражениями try...catch
, наподобие приведённого ниже, возникает проблема.
function asyncHandler(fn) {
try {
setTimeout(function () {
fn();
}, 1);
} catch (e) { }
}
Развитие событий по версии модульного теста:
it('does not catch exceptions with errors', function () {
var fn = function () {
throw new TypeError('type error');
};
failedPromise(function() {
target(fn);
}).should.be.rejectedWith(TypeError);
});
function failedPromise(fn) {
return new Promise(function(resolve, reject) {
reject(fn);
});
}
Пришлось завернуть в обработчик промис проверки исключения. Обратите внимание, что здесь имеет место необработанное исключение, несмотря на наличие блока кода вокруг замечательного try...catch
. К сожалению, выражения try...catch
работают только с одиночным исполняемым контекстом. И к моменту выброса исключения интерпретатор уже перешёл к другой части кода, оставил try...catch
. Точно такая же ситуация возникает и с Ajax-вызовами.
Здесь у нас есть два пути. Первый — поймать исключение внутри асинхронного коллбэка:
setTimeout(function () {
try {
fn();
} catch (e) {
// Handle this async error
}
}, 1);
Это вполне рабочий вариант, но тут много чего можно улучшить. Во-первых, везде раскиданы блоки try...catch
— дань программированию 1970-х. Во-вторых, движок V8 [9] не слишком удачно использует эти блоки внутри функций [10], поэтому разработчики рекомендуют размещать try...catch
сверху стека вызовов.
Так что нам делать? Я не просто так упоминал о том, что глобальные обработчики ошибок работают с любым исполняемым контекстом. Если такой обработчик подписать на событие window.onerror, то больше ничего не нужно! У вас сразу начинают соблюдаться принципы DRY и SOLID.
Ниже представлен пример отчёта, отправляемого на сервер обработчиком исключений. Если вы будете прогонять демокод, то у вас этот отчёт может быть немного другим, в зависимости от используемого браузера.
Этот обработчик даже сообщает о том, что ошибка связана с асинхронным кодом, точнее с обработчиком setTimeout()
. Прямо сказка!
Есть как минимум два основных подхода к обработке ошибок. Первый — когда вы игнорируете ошибки, останавливая исполнение без уведомления. Второй — когда вы сразу получаете информацию об ошибке и отматываете до момента её возникновения. Думаю, всем очевидно, какой из этих подходов лучше и почему. Говоря кратко: не скрывайте проблемы. Никто не будет винить вас за возможные сбои в программе. Вполне допустимо останавливать исполнение, откатывать состояние и давать пользователю новую попытку. Мир несовершенен, поэтому важно давать второй шанс. Ошибки неизбежны, и значение имеет только то, как вы с ними справляетесь.
Автор: Mail.Ru Group
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/118767
Ссылки в тексте:
[1] закон Мёрфи: https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%BA%D0%BE%D0%BD_%D0%9C%D0%B5%D1%80%D1%84%D0%B8
[2] Исключительная обработка событий в JavaScript: http://www.sitepoint.com/exceptional-exception-handling-in-javascript/
[3] GitHub: https://github.com/sitepoint-editors/ProperErrorHandlingJavaScript
[4] Mocha: http://mochajs.org/
[5] should.js: http://shouldjs.github.io/
[6] SOLID: https://ru.wikipedia.org/wiki/SOLID_%28%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%29
[7] стека: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Stack
[8] DRY: https://ru.wikipedia.org/wiki/Don%E2%80%99t_repeat_yourself
[9] движок V8: https://ru.wikipedia.org/wiki/V8_%28%D0%B4%D0%B2%D0%B8%D0%B6%D0%BE%D0%BA_JavaScript%29
[10] использует эти блоки внутри функций: https://github.com/nodejs/node-v0.x-archive/wiki/Best-practices-and-gotchas-with-v8
[11] Источник: https://habrahabr.ru/post/282149/
Нажмите здесь для печати.