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

Лучшие проекты JavaScript для начинающих Изучение

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

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

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

  • Хороший редактор или линтер может отлавливать синтаксические ошибки.
  • Хорошая проверка может выявить ошибки пользовательского ввода.
  • Надежные процессы тестирования могут обнаруживать логические ошибки.

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

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

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

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

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

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

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

Читайте также:  Snowflake 101: Знакомство с облаком данных Snowflake

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

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

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

}

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

// show result of division
function showResult() {

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

}

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

  • он находит обработчик исключений
  • он достигает верхнего уровня кода (что приводит к завершению программы и отображению ошибки в консоли, как показано в примере CodePen выше)

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

Мы можем добавить обработчик исключений в 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 {}блок выполняется и получает выброшенный объект ошибки. Как и прежде, попробуйте установить десятичные разряды на отрицательное число в этой демонстрации CodePen.

Результат теперь показывает 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оператор, это значение становится возвращаемым значением для всей функции; другие 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блок никогда не достигается, потому что catchблок в divide()функции обрабатывает ошибку.

Однако мы могли бы программно добавить новый 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 создает и выдает объект, описывающий ошибку, используя один из следующих типов.

Ошибка синтаксиса

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

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

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

ReferenceError

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

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

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

Ошибка типа

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

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()! Выполнение произвольного кода, содержащегося в строке, возможно, созданной на основе пользовательского ввода, слишком опасно!

Агрегатеррор

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

Внутренняя ошибка

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

Ошибка

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

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

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

  • нашей функции не переданы допустимые параметры
  • запрос Ajax не возвращает ожидаемые данные
  • обновление DOM завершается ошибкой, поскольку узел не существует

Оператор 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функцию like без 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блоке:

  • .name: название типа ошибки — например, ErrorилиRangeError
  • .message: сообщение об ошибке

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

  • .fileName: файл, в котором произошла ошибка
  • .lineNumber: номер строки, где произошла ошибка
  • .columnNumber: номер столбца в строке, где произошла ошибка
  • .stack: трассировка стека, в которой перечислены вызовы функций, сделанные до возникновения ошибки.

Мы можем изменить divide()функцию, чтобы она бросала a 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 возвращает бесконечность при делении на ноль, но это может запутать пользователей. Вместо создания универсального Error, мы могли бы создать собственный 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()функции. Он может принимать любой Error тип и реагировать соответствующим образом — в данном случае, показывая сообщение об ошибке:

// 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 );
  }

}

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

// 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блок вокруг final 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, заключается в том, чтобы возвращать ошибку в качестве первого параметра функции обратного вызова. Это не приведет к возникновению исключения, хотя Errorпри необходимости мы можем создать его вручную:

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для вызова любой функции, которая возвращает обещание. Это должно происходить внутри asyncфункции, но мы можем перехватывать ошибки с помощью стандартного try…catchблока.

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

(async () => {

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

})();

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

Оцените статью
bestprogrammer.ru
Добавить комментарий