Ах, опасности обработки ошибок в JavaScript. Если вы верите закону Мерфи , все, что может пойти не так, пойдет не так. В этой статье я хотел бы изучить обработку ошибок в JavaScript. Я расскажу о подводных камнях, передовых методах и закончу с асинхронным кодом и Ajax.
Эта популярная статья была обновлена 08.06.2017 для обратной связи с читателями. В частности, в фрагменты были добавлены имена файлов, очищены модульные тесты, в
uglyHandler
был добавлен шаблон оболочки, добавлены разделы по CORS и сторонним обработчикам ошибок.
Я чувствую, что управляемая событиями парадигма JavaScript добавляет языку богатство. Мне нравится представлять браузер как машину, управляемую событиями, и ошибки ничем не отличаются. Когда происходит ошибка, событие генерируется в какой-то момент. Теоретически можно утверждать, что ошибки — это простые события в JavaScript.
Если это звучит для вас чуждо, пристегнитесь, чтобы подвезти. В этой статье я остановлюсь только на клиентском JavaScript.
Этот раздел основан на концепциях, описанных в разделе Обработка исключительных исключений в JavaScript . Я рекомендую прочитать основы, если вы не знакомы. Эта статья также предполагает средний уровень знаний JavaScript. Если вы хотите повысить свой уровень, почему бы не зарегистрироваться в SitePoint Premium и посмотреть наш курс JavaScript: Следующие шаги . Первый урок бесплатный.
В любом случае, моя цель состоит в том, чтобы исследовать не только самые необходимые вещи для обработки исключений. Чтение этой статьи заставит вас подумать дважды, когда в следующий раз вы увидите хороший блок try...catch
.
Демо
Демонстрация, которую мы будем использовать для этой статьи, доступна на GitHub и представляет такую страницу:
Все кнопки детонируют «бомбу» при нажатии. Эта бомба имитирует исключение, которое выдается как TypeError
. Ниже приведено определение такого модуля:
// scripts/error.js function error() { var foo = {}; return foo.bar(); }
Для начала эта функция объявляет пустой объект с именем foo
. Обратите внимание, что bar()
нигде не получает определения. Давайте проверим, что это взорвёт бомбу с помощью хорошего юнит-теста:
// tests/scripts/errorTest.js it('throws a TypeError', function () { should.throws(error, TypeError); });
Этот модульный тест выполнен в Mocha с проверочными утверждениями в Should.js . Mocha — тестовый прогон, а Should.js — библиотека утверждений. Не стесняйтесь исследовать API тестирования, если вы еще не знакомы. Тест начинается с it('description')
и заканчивается «пройти / не пройти» в случае should
. Модульные тесты выполняются на узле и не нуждаются в браузере. Я рекомендую обратить внимание на тесты, поскольку они подтверждают ключевые концепции в простом JavaScript.
После того, как вы клонировали репозиторий и установили зависимости, вы можете запустить тесты, используя
npm t
. Кроме того, вы можете запустить этот отдельный тест следующим образом:./node_modules/mocha/bin/mocha tests/scripts/errorTest.js
.
Как показано, error()
определяет пустой объект, затем пытается получить доступ к методу. Поскольку bar()
не существует внутри объекта, он генерирует исключение. Поверьте, с динамическим языком, таким как JavaScript, это происходит со всеми!
Плохо
На некоторые плохие обработки ошибок. Я абстрагировал обработчик кнопки от реализации. Вот как выглядит обработчик:
// scripts/badHandler.js function badHandler(fn) { try { return fn(); } catch (e) { } return null; }
Этот обработчик получает обратный вызов fn
в качестве параметра. Этот обратный вызов затем вызывается внутри функции обработчика. Модульные тесты показывают, насколько это полезно:
// tests/scripts/badHandlerTest.js it('returns a value without errors', function() { var fn = function() { return 1; }; var result = badHandler(fn); result.should.equal(1); }); it('returns a null with errors', function() { var fn = function() { throw new Error('random error'); }; var result = badHandler(fn); should(result).equal(null); });
Как видите, этот плохой обработчик ошибок возвращает null
если что-то идет не так. Обратный вызов fn()
может указывать на допустимый метод или бомбу.
Приведенный ниже обработчик события click рассказывает остальную часть истории:
// scripts/badHandlerDom.js (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. Этот злой обработчик проглатывает ошибки в коде и делает вид, что все хорошо. Это может быть хорошо с организациями, которые не заботятся о качестве кода. Но сокрытие ошибок приведет к тому, что в будущем вы будете отлаживаться часами. В многослойном решении с глубокими стеками вызовов невозможно определить, где это пошло не так. Что касается обработки ошибок, это довольно плохо.
Отказоустойчивая стратегия оставит вас в ожидании лучшей обработки ошибок. JavaScript предлагает более элегантный способ работы с исключениями.
Гадкий
Время исследовать уродливого обработчика. Я пропущу ту часть, которая тесно связана с DOM. Здесь нет разницы с плохим обработчиком, которого вы видели.
// scripts/uglyHandler.js function uglyHandler(fn) { try { return fn(); } catch (e) { throw new Error('a new error'); } }
Важно то, как он обрабатывает исключения, как показано ниже в этом модульном тесте:
// tests/scripts/uglyHandlerTest.js it('returns a new error with errors', function () { var fn = function () { throw new TypeError('type error'); }; should.throws(function () { uglyHandler(fn); }, Error); });
Определенное улучшение по сравнению с плохим обработчиком. Здесь исключение проходит через стек вызовов. Что мне нравится, так это теперь ошибки раскручивают стек, что очень полезно при отладке. За исключением, интерпретатор перемещается вверх по стеку в поисках другого обработчика. Это открывает множество возможностей для устранения ошибок в верхней части стека вызовов. К сожалению, так как это уродливый обработчик, я теряю исходную ошибку. Поэтому я вынужден пройти обратно вниз по стеку, чтобы выяснить исходное исключение. С этим, по крайней мере, я знаю, что что-то пошло не так, поэтому вы бросаете исключение.
В качестве альтернативы возможно завершить уродливый обработчик с пользовательской ошибкой. Когда вы добавляете больше деталей к ошибке, это уже не уродливо, но полезно. Ключ должен добавить конкретную информацию об ошибке.
Например:
// scripts/specifiedError.js // Create a custom error var SpecifiedError = function SpecifiedError(message) { this.name = 'SpecifiedError'; this.message = message || ''; this.stack = (new Error()).stack; }; SpecifiedError.prototype = new Error(); SpecifiedError.prototype.constructor = SpecifiedError;
// scripts/uglyHandlerImproved.js function uglyHandlerImproved(fn) { try { return fn(); } catch (e) { throw new SpecifiedError(e.message); } }
// tests/scripts/uglyHandlerImprovedTest.js it('returns a specified error with errors', function () { var fn = function () { throw new TypeError('type error'); }; should.throws(function () { uglyHandlerImproved(fn); }, SpecifiedError); });
Указанная ошибка добавляет дополнительные сведения и сохраняет исходное сообщение об ошибке. С этим улучшением это больше не уродливый обработчик, но чистый и полезный.
С этими обработчиками я все еще получаю необработанное исключение. Давайте посмотрим, есть ли в браузере что-то для решения этой проблемы.
Размотайте этот стек
Таким образом, один из способов отменить исключения — это поместить try...catch
в верхнюю часть стека вызовов.
Скажем, например:
function main(bomb) { try { bomb(); } catch (e) { // Handle all the error things } }
Но помните, я сказал, что браузер управляется событиями? Да, исключение в JavaScript не более чем событие. Интерпретатор останавливает выполнение в контексте выполнения и раскручивает. Оказывается, есть глобальный обработчик событий onerror, который мы можем использовать.
И это выглядит примерно так:
// scripts/errorHandlerDom.js window.addEventListener('error', function (e) { var error = e.error; console.log(error); });
Этот обработчик событий ловит ошибки в любом выполняющемся контексте. События ошибок запускаются из различных целей для любого типа ошибки. Что настолько радикально, так это то, что этот обработчик событий централизует обработку ошибок в коде. Как и с любым другим событием, вы можете обработать последовательные цепочки для обработки определенных ошибок. Это позволяет обработчикам ошибок иметь одну цель, если вы следуете принципам SOLID . Эти обработчики могут быть зарегистрированы в любое время. Интерпретатор будет перебирать столько обработчиков, сколько ему нужно. Кодовая база освобождается от блоков try...catch
которые перетираются повсюду, что облегчает отладку. Ключ должен относиться к обработке ошибок как к обработке событий в JavaScript.
Теперь, когда есть способ размотать стек с помощью глобальных обработчиков, что мы можем сделать с этим?
В конце концов, пусть стек вызовов будет с вами.
Захватить стек
Стек вызовов очень полезен при устранении неполадок. Хорошей новостью является то, что браузер предоставляет эту информацию из коробки. Свойство стека не является частью стандарта, но оно постоянно доступно в последних браузерах.
Так, например, теперь вы можете регистрировать ошибки на сервере:
// scripts/errorAjaxHandlerDom.js 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); // Fire an Ajax request with error details xhr.send(message); });
Это может быть неочевидно из этого примера, но это сработает вместе с предыдущим примером. Каждый обработчик ошибок может иметь одну цель, которая хранит код СУХОЙ .
В браузере обработчики событий добавляются в DOM. Это означает, что если вы создаете стороннюю библиотеку, ваши события будут сосуществовать с клиентским кодом. window.addEventListener()
позаботится об этом за вас, он не удалит существующие события.
Вот скриншот того, как этот журнал выглядит на сервере:
Этот журнал находится в командной строке, да, он непростительно работает в Windows.
Это сообщение взято из Firefox Developer Edition 54. Обратите внимание, что при наличии надлежащего обработчика ошибок совершенно ясно, в чем проблема. Не нужно скрывать ошибки, взглянув на это, я вижу, что выкинуло исключение и куда. Этот уровень прозрачности хорош для отладки интерфейсного кода. Вы можете анализировать журналы, давая представление о том, какие условия вызывают какие ошибки.
Стек вызовов полезен для отладки, никогда не стоит недооценивать мощность стека вызовов.
Одно замечание: если у вас есть скрипт из другого домена и вы включили CORS, вы не увидите никаких деталей об ошибке. Это происходит, когда вы помещаете сценарии в CDN, например, чтобы использовать ограничение шести запросов на домен. В e.message
будет только «Ошибка e.message
», что плохо. В JavaScript информация об ошибках доступна только для одного домена.
Одно из решений состоит в том, чтобы повторно выдавать ошибки, сохраняя сообщение об ошибке:
try { return fn(); } catch (e) { throw new Error(e.message); }
Как только вы сбросите ошибку, ваши глобальные обработчики ошибок выполнят остальную работу. Только убедитесь, что ваши обработчики ошибок находятся в одном домене. Вы можете даже обернуть это вокруг пользовательской ошибки с определенной информацией об ошибке. Это сохраняет исходное сообщение, стек и пользовательский объект ошибки.
Асинхронная обработка
Ах, опасности асинхронности. JavaScript вырывает асинхронный код из контекста выполнения. Это означает, что обработчики исключений, такие как приведенный ниже, имеют проблему:
// scripts/asyncHandler.js function asyncHandler(fn) { try { // This rips the potential bomb from the current context setTimeout(function () { fn(); }, 1); } catch (e) { } }
Юнит-тест рассказывает остальную часть истории:
// tests/scripts/asyncHandlerTest.js it('does not catch exceptions with errors', function () { // The bomb var fn = function () { throw new TypeError('type error'); }; // Check that the exception is not caught should.doesNotThrow(function () { asyncHandler(fn); }); });
Исключение не попадает, и я могу проверить это с помощью этого модульного теста. Обратите внимание, что возникает необработанное исключение, хотя у меня есть код, обернутый вокруг хорошей try...catch
. Да, try...catch
операторы try...catch
работают только в пределах одного исполняемого контекста. К тому времени, когда возникает исключение, переводчик отошел от try...catch
. Такое же поведение происходит и с вызовами Ajax.
Итак, одна альтернатива — перехватывать исключения внутри асинхронного обратного вызова:
setTimeout(function () { try { fn(); } catch (e) { // Handle this async error } }, 1);
Этот подход будет работать, но он оставляет много возможностей для улучшения. Прежде всего, try...catch
блоки запутаться повсюду. На самом деле, 1970-е годы вызвали плохое программирование и они хотят вернуть свой код. Плюс, двигатель V8 препятствует использованию блоков try… catch внутри функций . V8 — это движок JavaScript, используемый в браузере Chrome и Node. Одна идея состоит в том, чтобы переместить блоки на вершину стека вызовов, но это не работает для асинхронного кода.
Итак, куда это нас ведет? Есть причина, по которой я сказал, что глобальные обработчики ошибок работают в любом контексте выполнения. Если вы добавили обработчик ошибок в объект окна, то все, готово! Приятно, что решение остаться сухим и твердым окупается. Глобальный обработчик ошибок сделает ваш асинхронный код красивым и чистым.
Ниже описано, что этот обработчик исключений сообщает на сервере. Обратите внимание, что если вы будете следовать, вывод, который вы увидите, будет различным в зависимости от того, какой браузер вы используете.
Этот обработчик даже говорит мне, что ошибка исходит из асинхронного кода. Он говорит, что это происходит из функции setTimeout()
. Слишком круто!
Вывод
В мире обработки ошибок существует как минимум два подхода. Одним из них является отказоустойчивый подход, при котором вы игнорируете ошибки в коде. Другой подход — быстрое и безотказное, когда ошибки останавливают мир и перематывают его. Думаю, понятно, за кого из них я выступаю и почему. Мое мнение: не скрывайте проблемы. Никто не будет стыдить вас за несчастные случаи, которые могут произойти в программе. Допустимо останавливать, перематывать и давать пользователям еще одну попытку.
В мире, который далек от совершенства, важно предусмотреть второй шанс. Ошибки неизбежны, важно то, что вы делаете с ними.
Эта статья была рецензирована Тимом Севериеном и Морицем Крегером . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!