Статьи

Руководство по правильной обработке ошибок в JavaScript

Ах, опасности обработки ошибок в JavaScript. Если вы верите закону Мерфи , все, что может пойти не так, пойдет не так. В этой статье я хотел бы изучить обработку ошибок в JavaScript. Я расскажу о подводных камнях, передовых методах и закончу с асинхронным кодом и Ajax.

Эта популярная статья была обновлена ​​08.06.2017 для обратной связи с читателями. В частности, в фрагменты были добавлены имена файлов, очищены модульные тесты, в uglyHandler был добавлен шаблон оболочки, добавлены разделы по CORS и сторонним обработчикам ошибок.

Я чувствую, что управляемая событиями парадигма JavaScript добавляет языку богатство. Мне нравится представлять браузер как машину, управляемую событиями, и ошибки ничем не отличаются. Когда происходит ошибка, событие генерируется в какой-то момент. Теоретически можно утверждать, что ошибки — это простые события в JavaScript.

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

Этот раздел основан на концепциях, описанных в разделе Обработка исключительных исключений в JavaScript . Я рекомендую прочитать основы, если вы не знакомы. Эта статья также предполагает средний уровень знаний JavaScript. Если вы хотите повысить свой уровень, почему бы не зарегистрироваться в SitePoint Premium и посмотреть наш курс JavaScript: Следующие шаги . Первый урок бесплатный.

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

Демо

Демонстрация, которую мы будем использовать для этой статьи, доступна на GitHub и представляет такую ​​страницу:

Обработка ошибок в JavaScript Demo

Все кнопки детонируют «бомбу» при нажатии. Эта бомба имитирует исключение, которое выдается как 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() позаботится об этом за вас, он не удалит существующие события.

Вот скриншот того, как этот журнал выглядит на сервере:

Запрос журнала Ajax на сервер узла

Этот журнал находится в командной строке, да, он непростительно работает в 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 как можно лучше!