JavaScript: Полное руководство по обработке ошибок

Источник: «The Ultimate Guide to JavaScript Error Handling»
В этом руководстве мы погрузимся в обработку ошибок JavaScript, чтобы вы могли выбрасывать исключения, обнаруживать и обрабатывать собственные ошибки.

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

Некоторых ошибок веб-приложений можно избежать, например:

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

Отображение сообщения об ошибке — крайняя мера

В идеале пользователи никогда не должны видеть сообщения об ошибке.

Мы можем игнорировать незначительные проблемы, такие как невозможность загрузки декоративного изображения. Мы могли бы решить более серьёзные проблемы, такие как сбои сохранения данных Ajax локально и загружая их позже. Ошибка становиться необходимой только тогда, когда пользователь рискует потерять данные, предполагая, что он может что-то с этим сделать.

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

Как JavaScript обрабатывает ошибки

Когда оператор JavaScript приводит к ошибке, говорят, что он генерирует (выбрасывает) исключение. JavaScript создаёт и выбрасывает объект Error, описывающий ошибку. Мы можем увидеть это в действии на CodePen. Если установить в десятичные разряды (decimal places) отрицательное число, мы увидим сообщение об ошибке в консоли внизу. (Обратите внимание, что мы не встраиваем CodePen в это руководство, потому что нужно иметь возможно видеть вывод консоли, чтобы этот пример имел смысл)

Результат не обновиться, и мы увидим сообщение RangeError в консоли. Следующая функция выдаёт ошибку, когда dp имеет отрицательно значение:

// division calculation
function divide(v1, v2, dp) {

return (v1 / v2).toFixed(dp);

}

После выдачи ошибки интерпретатор JavaScript проверяет наличие кода обработки исключений. В функции Division() ничего нет, поэтому она проверяет вызывающую функцию:

// show result of division
function showResult() {

result.value = divide(
parseFloat(num1.value),
parseFloat(num2.value),
parseFloat(dp.value)
);

}

Интерпретатор повторяет процесс для каждой функции в стеке вызовов, пока не произойдёт одно из следующих событий:

Перехват исключений

Мы можем добавить обработчик исключений в функцию divide() с помощью блока try...catch:

// division calculation
function divide(v1, v2, dp) {
try {
return (v1 / v2).toFixed(dp);
}
catch(e) {
console.log(`
error name :
${ e.name }
error message:
${ e.message }
`
);
return 'ERROR';
}
}

Функция выполняет код в блоке try {}, но при возникновении исключения выполняется блок catch {} и получает выброшенный объект ошибки. Как и прежде, попробуйте в decimal places установить отрицательное число в этой демонстрации CodePen.

Теперь result показывает ERROR. Консоль показывает имя ошибки и сообщение, но это выводится оператором console.log и не завершает работу программы.

Примечание: эта демонстрация блока try...catch излишняя для базовой функции, такой как divide(). Как мы увидим ниже, проще убедиться, что dp равен нулю или больше.

Можно определить не обязательный блок finally {}, если требуется, чтобы код запускался при выполнении кода try или catch:

function divide(v1, v2, dp) {
try {
return (v1 / v2).toFixed(dp);
}
catch(e) {
return 'ERROR';
}
finally {
console.log('done');
}
}

В консоль выведется done, независимо от того, успешно ли выполнено вычисление или возникла ошибка. Блок finally обычно выполняет действия, которые в противном случае нам пришлось бы повторять как в блоке try, так и в блоке catch. Например, отмену вызова API или закрытие соединения с базой данных.

Для блока try требуется либо блок catch, либо блок finally, либо и то и другое. Обратите внимание, что когда блок finally содержит оператор return, это значение становится возвращаемым значением для всей функции; другие операторы в блоках try или catch игнорируются.

Вложенные обработчики исключений

Что произойдёт, если мы добавим обработчик исключений к вызывающей функции showResult()?

// show result of division
function showResult() {

try {
result.value = divide(
parseFloat(num1.value),
parseFloat(num2.value),
parseFloat(dp.value)
);
}
catch(e) {
result.value = 'FAIL!';
}

}

Ответ… ничего! Блок catch никогда не выполняется, потому что в функции divide() блок catch обрабатывает ошибку.

Тем не менее мы могли бы программно генерировать новый объект Error в divide() и при желании передать исходную ошибку в свойстве cause второго аргумента:

function divide(v1, v2, dp) {
try {
return (v1 / v2).toFixed(dp);
}
catch(e) {
throw new Error('ERROR', { cause: e });
}
}

Это вызовет блок catch в вызывающей функции:

// show result of division
function showResult() {

try {
//...
}
catch(e) {
console.log( e.message ); // ERROR
console.log( e.cause.name ); // RangeError
result.value = 'FAIL!';
}
}

Стандартные типы ошибок JavaScript

Когда возникает исключение, JavaScript создаёт и выдаёт объект, описывающий ошибку, используя один из следующих типов.

SyntaxError

Ошибка, возникающая из-за синтаксически недопустимого кода, такого как отсутствующая скобка:

if condition) { // SyntaxError
console.log('condition is true');
}

Примечание: такие языки, как C++ и Java, сообщают об ошибках синтаксиса во время компиляции. JavaScript — интерпретируемый язык, поэтому синтаксические ошибки не выявляются до тех пор, пока код не запустится. Любой хороший редактор кода или линтер могут обнаружить синтаксические ошибки до того, как мы попытаемся запустить код.

ReferenceError

Ошибка при доступе к несуществующей переменной:

function inc() {
value++; // ReferenceError
}

Опять, хороший редактор кода или линтер могут обнаружить эту проблему.

TypeError

Ошибка возникает, когда значение не соответствует ожидаемому типу, например, при вызове несуществующего метода объекта:

const obj = {};
obj.missingMethod(); // TypeError

RangeError

Ошибка возникает, когда значение не входит в набор или диапазон допустимых значений. Используемый выше метод toFixed() генерирует эту ошибку, потому что он ожидает значение от 0 до 100:

const n = 123.456;
console.log( n.toFixed(-1) ); // RangeError

URIError

Ошибка выдаваемая функциями обработки URI, такими как encodeURI() и decodeURI(), при обнаружении неправильных URI:

const u = decodeURIComponent('%'); // URIError

EvalError

Ошибка возникающая при передаче строки, содержащей не валидный JavaScript код, в функцию eval():

eval('console.logg x;'); // EvalError

Примечание: пожалуйста, не используйте eval()! Выполнение произвольного кода, содержащегося в строке, возможно, созданной на основе пользовательского ввода, слишком опасно!

AggregateError

Ошибка возникает, когда несколько ошибок объединены в одну ошибку. Обычно возникает при вызове такой операции, как Promise.all(), которая возвращает результаты нескольких промисов.

InternalError

Нестандартная ошибка (только в Firefox) возникает при возникновении внутренней ошибки движка JavaScript. Обычно это результат того, что что-то занимает слишком много памяти, например, большой массив или слишком много рекурсии.

Error

Наконец, есть общий объект Error, чаще всего используемый при реализации собственных исключений... о котором мы поговорим дальше.

Генерация/выбрасывание собственных исключений

Мы можем использовать throw для генерации/выбрасывания собственных исключений, когда возникает ошибка — или должна произойти. Например:

Оператор throw фактически принимает любое значение или объект. Например:

throw 'A simple error string';
throw 42;
throw true;
throw { message: 'An error', name: 'MyError' };

Исключения генерируются для каждой функции в стеке вызовов до тех пор, пока они не будут перехвачены обработчиком исключений (catch). Однако на практике мы хотим создать и сгенерировать объект Error, чтобы он действовал идентично стандартным ошибкам, выдаваемым JavaScript.

Можно создать общий объект Error, передав необязательное сообщение конструктору:

throw new Error('An error has occurred');

Так же Error можно использовать как функцию, без new. Она возвращает объект Error, идентичный приведённому выше:

throw Error('An error has occurred');

При желании можно передать имя файла и номер строки в качестве второго и третьего параметров:

throw new Error('An error has occurred', 'script.js', 99);

В этом редко возникает необходимость, так как по умолчанию они относятся к файлу и строке, где мы вызвали объект Error. (Также их сложно поддерживать, поскольку наши файлы меняются!)

Мы можем определить общие объекты Error, но по возможности следует использовать стандартный тип Error. Например:

throw new RangeError('Decimal places must be 0 or greater');

Все объекты Error имеют следующие свойства, которые можно проверить в блоке catch:

В Firefox поддерживаются следующие нестандартные свойства:

Мы можем изменить функцию divide() так, чтобы она вызывала ошибку RangeError, когда количество знаков после запятой не является числом, меньше нуля и больше восьми:

// division calculation
function divide(v1, v2, dp) {

if (isNaN(dp) || dp < 0 || dp > 8) {
throw new RangeError('Decimal places must be between 0 and 8');
}

return (v1 / v2).toFixed(dp);
}

Точно так же мы могли бы выдать Error или TypeError, когда значения делимого не является числом, чтобы предотвратить результат NaN:

  if (isNaN(v1)) {
throw new TypeError('Dividend must be a number');
}

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

// new DivByZeroError Error type
class DivByZeroError extends Error {
constructor(message) {
super(message);
this.name = 'DivByZeroError';
}
}

Затем вызывать/выбрасывать его подобным образом:

if (isNaN(v2) || !v2) {
throw new DivByZeroError('Divisor must be a non-zero number');
}

Теперь добавьте блок try...catch к вызывающей функции showResult(). Он сможет получить тип любой ошибки и отреагировать соответствующим образом — в данном случае, выводя сообщение об ошибке:

// show result of division
function showResult() {

try {
result.value = divide(
parseFloat(num1.value),
parseFloat(num2.value),
parseFloat(dp.value)
);
errmsg.textContent = '';
}
catch (e) {
result.value = 'ERROR';
errmsg.textContent = e.message;
console.log( e.name );
}

}

Попробуйте ввести недопустимые нечисловые, нулевые и отрицательные значения в демонстрации на CodePen.

Окончательная версия функции divide() проверяет все входящие значения и при необходимости выдаёт соответствующую ошибку:

// division calculation
function divide(v1, v2, dp) {

if (isNaN(v1)) {
throw new TypeError('Dividend must be a number');
}

if (isNaN(v2) || !v2) {
throw new DivByZeroError('Divisor must be a non-zero number');
}

if (isNaN(dp) || dp < 0 || dp > 8) {
throw new RangeError('Decimal places must be between 0 and 8');
}

return (v1 / v2).toFixed(dp);
}

Больше нет необходимости размещать блок try...catch вокруг финального return, так как он никогда не должен генерировать ошибку. Если бы это произошло, JavaScript сгенерировал бы свою собственную ошибку и обработал бы её блоком catch в showResult()/

Ошибки асинхронной функции

Мы не можем перехватывать исключения, генерируемые асинхронными функциями на основе обратного вызова, потому что после завершения выполнения блока try...catch выдаётся ошибка. Этот код выглядит правильно, но блок catch никогда не выполнится, и через секунду консоль отобразит сообщение Uncaught Error:

function asyncError(delay = 1000) {

setTimeout(() => {
throw new Error('I am never caught!');
}, delay);

}

try {
asyncError();
}
catch(e) {
console.error('This will never run');
}

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

function asyncError(delay = 1000, callback) {

setTimeout(() => {
callback('This is an error message');
}, delay);

}

asyncError(1000, e => {

if (e) {
throw new Error(`error: ${ e }`);
}

});

Ошибки на основе промисов

Обратные вызовы могут стать громоздкими, поэтому при написании асинхронного кода предпочтительнее использовать промисы. При возникновении ошибки метод reject() промиса может вернуть новый объект Error или любое другое значение:

function wait(delay = 1000) {

return new Promise((resolve, reject) => {

if (isNaN(delay) || delay < 0) {
reject( new TypeError('Invalid delay') );
}
else {
setTimeout(() => {
resolve(`waited ${ delay } ms`);
}, delay);
}

})

}

Примечание: функции должны быть либо 100% синхронными, либо 100% асинхронными. Вот почему необходимо проверять значение delay внутри возвращаемого промиса. Если бы мы проверили значение delay и выдали ошибку перед возвратом промиса, функция стала бы синхронной при возникновении ошибки.

Метод Promise.catch() выполняется при передаче недопустимого параметра delay и получает возвращённый объект Error:

// invalid delay value passed
wait('INVALID')
.then( res => console.log( res ))
.catch( e => console.error( e.message ) )
.finally( () => console.log('complete') );

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

Следующая (вызываемая немедленно) асинхронная функция функционально идентична цепочке промисов выше:

(async () => {

try {
console.log( await wait('INVALID') );
}
catch (e) {
console.error( e.message );
}
finally {
console.log('complete');
}

})();

Исключительная обработка исключения

Выбрасывать объекты Error и обрабатывать исключения в JavaScript легко:

try {
throw new Error('I am an error!');
}
catch (e) {
console.log(`error ${ e.message }`)
}

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

Дополнительная информация:

Дополнительные материалы

Предыдущая Статья

CORS и заголовок ответа Access-Control-Allow-Origin

Следующая Статья

Совместное использование правил PHPCS между проектами и командами