Разберитесь со всеми различными способами обработки ошибок в JavaScript.

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

Некоторых ошибок можно избежать:

  • Линтер JavaScript или хороший редактор могут обнаруживать синтаксические ошибки, такие как операторы с ошибками и отсутствующие скобки.
  • Вы можете использовать проверку для обнаружения ошибок в данных от пользователей или других систем. Никогда не делайте предположений об изобретательности пользователя, чтобы вызвать хаос. Вы можете ожидать целое число, когда спрашиваете чей-то возраст, но может ли он оставить поле пустым, ввести отрицательное значение, использовать дробное значение или даже полностью ввести «двадцать один» на своем родном языке.
  • Помните, что проверка на стороне сервера необходима. Проверка на стороне клиента на основе браузера может улучшить пользовательский интерфейс, но пользователь может использовать приложение, в котором JavaScript отключен, не загружается или не выполняется. Возможно, ваш API вызывает не браузер!

Других ошибок во время выполнения избежать невозможно:

  • сеть может выйти из строя
  • занятому серверу или приложению может потребоваться слишком много времени для ответа
  • срок действия скрипта или другого актива может истечь
  • приложение может не работать
  • диск или база данных могут выйти из строя
  • серверная ОС может выйти из строя
  • инфраструктура хоста может выйти из строя

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

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

Мы все сталкивались с ошибками в приложениях. Некоторые из них полезны:

«Файл уже существует. Хотите перезаписать его?»

Другие меньше:

"ОШИБКА 5969"

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

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

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

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

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

const e = new Error(‘An error has occurred’);

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

const e = Error('An error has occurred');

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

const e = new Error('An error has occurred', 'script.js', 99);

В этом редко возникает необходимость, так как по умолчанию используется строка, в которой вы создали объект Error в текущем файле.

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

  • .name: имя типа Error (в данном случае "Error")
  • .message: сообщение об ошибке

Следующие свойства чтения/записи также поддерживаются в Firefox:

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

Типы ошибок

Помимо общего Error, JavaScript поддерживает определенные типы ошибок:

  • EvalError: вызвано eval()
  • RangeError: значение вне допустимого диапазона
  • ReferenceError: возникает при разыменовании недопустимой ссылки
  • SyntaxError: неверный синтаксис
  • TypeError: значение недопустимого типа
  • URIError: неверные параметры переданы в encodeURI() или decodeURI()
  • AggregateError: несколько ошибок, заключенных в одну ошибку, которая обычно возникает при вызове операции, такой как Promise.all().

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

Создание исключения

Создание объекта Error само по себе ничего не делает. Вы должны Error вызвать исключение:

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

Эта функция sum() выдает TypeError, когда любой из аргументов не является числом — return никогда не выполняется:

function sum(a, b) {
  if (isNaN(a) || isNaN(b)) {
    throw new TypeError('Value is not a number.');
  }
  return a + b;
}

Практично использовать throw объект Error, но вы можете использовать любое значение или объект:

throw 'Error string';
throw 42;
throw true;
throw { message: 'An error', name: 'CustomError' };

Когда вы throw создаете исключение, оно всплывает в стеке вызовов, если оно не перехвачено. Неперехваченные исключения в конечном итоге достигают вершины стека, где программа останавливается и показывает ошибку в консоли DevTools, например.

Uncaught TypeError: Value is not a number.
  sum https://mysite.com/js/index.js:3

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

Вы можете ловить исключения в блоке try ... catch:

try {
  console.log( sum(1, 'a') );
}
catch (err) {
  console.error( err.message );
}

При этом выполняется код в блоке try {}, но при возникновении исключения блок catch {} получает объект, возвращенный блоком throw.

Блок catch может анализировать ошибку и реагировать соответствующим образом, например.

try {
  console.log( sum(1, 'a') );
}
catch (err) {
  if (err instanceof TypeError) {
    console.error( 'wrong type' );
  }
  else {
    console.error( err.message );
  }
}

Вы можете определить необязательный блок finally {}, если вам требуется код для запуска независимо от того, выполняется ли код try или catch. Это может быть полезно при очистке, например. чтобы закрыть соединение с базой данных в Node.js или Deno:

try {
  console.log( sum(1, 'a') );
}
catch (err) {
  console.error( err.message );
}
finally {
  // this always runs
  console.log( 'program has ended' );
}

Для блока try требуется либо блок catch, либо блок finally, либо оба.

Обратите внимание, что когда блок finally содержит return, это значение становится возвращаемым значением для всего try ... catch ... finally независимо от каких-либо операторов return в блоках try и catch.

Повтор сеанса с открытым исходным кодом

Отладка веб-приложения в рабочей среде может быть сложной и трудоемкой. OpenReplay — это альтернатива FullStory, LogRocket и Hotjar с открытым исходным кодом. Это позволяет вам отслеживать и воспроизводить все, что делают ваши пользователи, и показывает, как ваше приложение ведет себя при каждой проблеме. Это похоже на то, как если бы инспектор вашего браузера был открыт, когда вы смотрите через плечо вашего пользователя. OpenReplay — единственная доступная в настоящее время альтернатива с открытым исходным кодом.

Удачной отладки, для современных фронтенд-команд — Начните бесплатно отслеживать свое веб-приложение.

Вложенныеtry … catchБлоки и повторная выдача ошибок

Исключение всплывает вверх по стеку и перехватывается один раз только ближайшим блоком catch, например

try {
  try {
    console.log( sum(1, 'a') );
  }
  catch (err) {
    console.error('This error will trigger', err.message);
  }
}
catch (err) {
  console.error('This error will not trigger', err.message);
}

Любой catch блок может throw создать новое исключение, которое перехватывается внешним catch. Вы можете передать первый объект Error новому Error в свойстве cause объекта, переданного конструктору. Это дает возможность поднять и изучить цепочку ошибок.

В этом примере выполняются оба блока catch, потому что первая ошибка вызывает вторую:

try {
  try {
    console.log( sum(1, 'a') );
  }
  catch (err) {
    console.error('First error caught', err.message);
    throw new Error('Second error', { cause: err });
  }
}
catch (err) {
  console.error('Second error caught', err.message);
  console.error('Error cause:', err.cause.message);
}

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

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

function wait(delay = 1000) {
setTimeout(() => {
    throw new Error('I am never caught!');
  }, delay);
}
try {
  wait();
}
catch(err) {
  // this will never run
  console.error('caught!', err.message);
}

По истечении одной секунды на консоли отображается:

Uncaught Error: I am never caught!
  wait http://server.com/script.js:3

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

function wait(delay = 1000, callback) {
  setTimeout(() => {
    callback('I am caught!');
  }, delay);
}
wait(1000, (err) => {
  if (err) {
    throw new Error(err);
  }
});

В современном ES6 часто лучше возвращать Promise при определении асинхронных функций. При возникновении ошибки метод Promise 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);
    }
  })
}

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

// this fails - the catch block is run
wait('x')
  .then( res => console.log( res ))
  .catch( err => console.error( err.message ))
  .finally(() => console.log('done'));

Любая функция, которая возвращает Promise, может быть вызвана функцией async с использованием оператора await. Вы можете поместить это в блок try ... catch, который работает идентично приведенному выше примеру .then/.catch Promise, но может быть немного проще для чтения:

// Immediately-Invoked (Asynchronous) Function Expression
(async () => {
  try {
    console.log( await wait('x') );
  }
  catch (err) {
    console.error( err.message );
  }
  finally {
    console.log('done');
  }
})();

Ошибки неизбежны

В JavaScript легко создавать объекты ошибок и вызывать исключения. Правильно реагировать и создавать устойчивые приложения несколько сложнее! Лучший совет: ожидайте неожиданного и исправляйте ошибки как можно скорее.

Первоначально опубликовано на https://blog.openreplay.com 21 февраля 2022 г.