К списку уроков

Урок 7. Асинхронность: вкладка Network, Promise, async/await и обработка ошибок

Онлайн и в Б-3, 19 октября (понедельник), 15:00

Предисловие

Для вашего удобства текстовый курс перенесен на отдельную платформу. Ссылка для поступления: тык

Мы настоятельно рекомендуем пользоваться именно этой платформой по ходу курса. Ссылка на текущее занятие: тык

⚠️Прежде чем переходить к заданиям - обязательно зарегистрируйтесь на курс. Иначе вас закинет на общий поток и вы не сможете взаимодействовать с сверстниками во втором модуле курса. Это не смертельно и вполне решаемо, но тем не менее.

Асинхронность: вкладка Network, Promise, async/await и обработка ошибок

Вкладка Network

Когда страница не получает данные, причину ищут не в коде, а во вкладке Network инструментов разработчика. Она показывает каждый запрос страницы: куда он ушёл, что в нём было и что ответил сервер.

Откройте инструменты разработчика, перейдите на вкладку Network и обновите страницу. Запросы появятся в таблице:

Колонка

Что показывает

Name

Последнюю часть адреса. Полный адрес виден при наведении курсора

Status

Статус-код ответа. Ошибки выделены красным

Type

Тип запроса: document, stylesheet, script, fetch, img

Size

Размер ответа

Time

Сколько времени занял запрос

Страница отправляет много запросов: за HTML, стилями, скриптами, картинками, шрифтами. Чтобы оставить в таблице только запросы к API, нажмите фильтр Fetch/XHR над таблицей. Поле Filter рядом с ним оставляет запросы, в адресе которых есть введённый текст.

По щелчку на заголовок колонки таблица сортируется. Сортировка по колонке Status поднимает ошибки наверх, а сортировка по колонке Time показывает самые медленные запросы.

Вкладка записывает запросы, только пока открыта. Если открыть её после загрузки страницы, таблица будет пустой, и страницу придётся обновить.

Почитать ещё

Подробности запроса

Если щёлкнуть по запросу в таблице Network, откроется панель с подробностями. Она разделена на вкладки.

  • Headers — адрес запроса, метод, статус-код, заголовки запроса и заголовки ответа. Здесь проверяют, ушёл ли запрос на правильный адрес и есть ли в нём заголовок Content-Type.

  • Payload — тело запроса и параметры адреса. Здесь проверяют, какие данные отправились на сервер. Вкладка появляется, только если у запроса есть тело или параметры.

  • Preview — тело ответа в удобном виде: JSON показан деревом, которое можно разворачивать.

  • Response — тело ответа как текст, без обработки.

  • Timing — из чего сложилось время запроса: сколько ушло на подключение, сколько запрос ждал ответа сервера и сколько скачивался ответ.

У запроса есть контекстное меню. Пункт Copy → Copy as fetch копирует готовый вызов fetch с тем же адресом, методом, заголовками и телом. Его можно вставить в консоль и повторить запрос, не повторяя действий на странице.

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

Почитать ещё

Медленная сеть и работа без интернета

На вашем компьютере запросы к серверу выполняются быстро, а у пользователя может быть медленный мобильный интернет. Вкладка Network позволяет проверить, как страница ведёт себя в таких условиях.

  • Throttling — выпадающий список с надписью «No throttling» над таблицей. В нём можно выбрать медленную сеть, например «Slow 4G» или «3G». Запросы начнут выполняться медленно, и вы увидите, что пользователь видит, пока данные загружаются.

  • Offline — вариант в том же списке. Он отключает сеть для страницы. Так проверяют, что при недоступном сервере страница показывает понятное сообщение, а не пустое место.

  • Disable cache — флажок, который запрещает браузеру брать файлы из кэша, пока открыты инструменты разработчика. Включайте его, если вы изменили CSS или JavaScript, а страница продолжает работать по-старому.

  • Preserve log — флажок, который сохраняет список запросов при переходе на другую страницу или перезагрузке. Нужен, когда запрос отправляется перед самым переходом и иначе пропадает из таблицы.

Режимы Throttling и Offline действуют, только пока открыты инструменты разработчика. Не забывайте выключать их: иначе можно долго искать причину, по которой страница вдруг перестала загружаться.

Почитать ещё

Как найти проблему с запросом

Когда данные не загружаются или не сохраняются, пройдите по списку вопросов. Ответ на каждый из них даёт вкладка Network.

  1. Есть ли запрос в таблице? Если запроса нет, код не дошёл до вызова fetch. Откройте консоль: скорее всего, до запроса произошла ошибка.

  2. Правильный ли адрес? Опечатка в пути — самая частая причина статуса 404.

  3. Какой статус-код? Код 4xx означает проблему в запросе: неправильные данные, заголовки или адрес. Код 5xx означает проблему на сервере. Если вместо кода красная надпись без числа, запрос не дошёл до сервера или браузер заблокировал ответ, и подробности будут в консоли.

  4. Что отправилось? Во вкладке Payload проверьте, что тело запроса совпадает с тем, что вы собирали в коде.

  5. Что пришло в ответ? Во вкладке Preview проверьте, что данные лежат там, откуда их читает код.

Если запрос отправился, получил статус 200, а данные в ответе правильные, но страница их не показывает, проблема в коде после получения ответа: в записи в состояние или в рендере. Тогда переходите к консоли и выводите промежуточные значения.

Слишком много одинаковых запросов подряд обычно означает, что запрос отправляется в цикле или обработчик события назначен несколько раз.

Почитать ещё

Почему запросы асинхронные

JavaScript на странице выполняется в одном потоке. Это значит, что в каждый момент времени выполняется только один фрагмент кода. Пока он выполняется, браузер не может ни обработать нажатие, ни перерисовать страницу.

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

Самый простой пример асинхронной операции — таймер setTimeout. Он вызывает функцию через заданное число миллисекунд:

console.log('Начали');

setTimeout(() => {
  console.log('Прошла секунда');
}, 1000);

console.log('Код после таймера');

Строка «Код после таймера» появилась раньше, чем «Прошла секунда»: setTimeout не ждёт секунду, а только запоминает, что через секунду нужно вызвать функцию.

Запросы fetch работают так же. Результат асинхронной операции не может появиться в следующей строке кода, поэтому нужен способ сказать: «когда результат будет готов, сделай вот это». В современном JavaScript таким способом служат промисы.

Почитать ещё

Промис

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

У промиса три состояния:

  • pending — ожидание: операция ещё выполняется;

  • fulfilled — выполнен: операция завершилась успешно, у промиса есть значение;

  • rejected — отклонён: операция завершилась ошибкой, у промиса есть причина ошибки.

Промис переходит из ожидания в одно из двух конечных состояний один раз, после этого его состояние не меняется.

Чтобы получить результат, у промиса вызывают методы:

fetch('https://dummyjson.com/products/1?select=title')
  .then((response) => response.json())
  .then((product) => {
    console.log('Выполнен:', product.title);
  })
  .catch((error) => {
    console.log('Отклонён:', error.message);
  })
  .finally(() => {
    console.log('Запрос завершён');
  });
  • .then(колбэк) вызывает колбэк, когда промис выполнен, и передаёт в него значение.

  • .catch(колбэк) вызывает колбэк, когда промис отклонён, и передаёт в него ошибку.

  • .finally(колбэк) вызывает колбэк в любом случае, после успеха или ошибки. Он удобен, чтобы убрать индикатор загрузки.

Промис, который вернул fetch, отклоняется, если ответ от сервера не получен: сеть недоступна или браузер заблокировал запрос. Ответ с любым статус-кодом, даже 500, считается успешным результатом, и промис выполняется.

Почитать ещё

Цепочки промисов

Метод .then возвращает новый промис, поэтому вызовы можно выстраивать в цепочку. Значение, которое вернул колбэк, становится значением нового промиса и попадает в следующий .then.

Promise.resolve(2)
  .then((value) => {
    console.log('Шаг 1 получил', value);
    return value * 10;
  })
  .then((value) => {
    console.log('Шаг 2 получил', value);
    return fetch(`https://dummyjson.com/products/${value}?select=title`);
  })
  .then((response) => response.json())
  .then((product) => {
    console.log('Шаг 4 получил', product.title);
  });

Promise.resolve(2) создаёт уже выполненный промис со значением 2, чтобы начать цепочку.

  • Если колбэк вернул обычное значение, следующий .then получит его сразу.

  • Если колбэк вернул промис, цепочка дождётся его результата, и следующий .then получит уже значение этого промиса. Так работает return response.json().

  • Если колбэк ничего не вернул, следующий .then получит undefined.

Ошибка на любом шаге пропускает все следующие .then и попадает в ближайший .catch:

Promise.resolve('старт')
  .then(() => {
    throw new Error('Что-то сломалось на первом шаге');
  })
  .then(() => {
    console.log('Этот шаг не выполнится');
  })
  .catch((error) => {
    console.log('Поймали:', error.message);
  });

Не вкладывайте .then внутрь другого .then. Если внутри колбэка нужно дождаться ещё одного промиса, верните его через return и продолжите цепочку на верхнем уровне.

Почитать ещё

Как создать промис

Чаще всего промисы получают готовыми, например из fetch. Но промис можно создать и самому, если нужно превратить в промис операцию, которая работает через колбэки.

function wait(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

function checkStock(quantity) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (quantity > 0) {
        resolve(`На складе ${quantity} шт.`);
      } else {
        reject(new Error('Товар закончился'));
      }
    }, 500);
  });
}

wait(1000).then(() => console.log('Прошла секунда'));

checkStock(3).then((message) => console.log(message));
checkStock(0).catch((error) => console.log(error.message));

Конструктор new Promise принимает функцию. Эта функция сразу получает два аргумента:

  • resolve(значение) переводит промис в состояние «выполнен» с этим значением;

  • reject(ошибка) переводит промис в состояние «отклонён» с этой ошибкой.

Функция wait из примера пригодится при отладке: await wait(2000) имитирует медленный ответ сервера. В реальном коде таких искусственных задержек быть не должно.

Почитать ещё

Несколько промисов сразу

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

const started = Date.now();

const getTitle = (id) =>
  fetch(`https://dummyjson.com/products/${id}?delay=1000&select=title`)
    .then((response) => response.json())
    .then((product) => product.title);

Promise.all([getTitle(1), getTitle(2), getTitle(3)])
  .then((titles) => {
    console.log(titles);
    console.log(`Прошло ${Date.now() - started} мс`);
  });

Параметр delay=1000 просит DummyJSON задержать каждый ответ на секунду. Три запроса выполнились одновременно, поэтому вместе они заняли чуть больше секунды, а не три.

  • Promise.all(массив промисов) возвращает промис, который выполнится, когда выполнятся все промисы из массива. Его значение — массив результатов в том же порядке, в котором были переданы промисы. Если хотя бы один промис отклонён, Promise.all сразу отклоняется с этой ошибкой.

  • Promise.allSettled(массив промисов) ждёт завершения всех промисов, независимо от успеха, и возвращает массив объектов с полями status и value или reason. Его используют, когда ошибка одного запроса не должна отменять остальные.

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

Почитать ещё

Событийный цикл

Раз JavaScript выполняется в одном потоке, результаты асинхронных операций не могут прервать код, который выполняется сейчас. Они ждут своей очереди. Порядок выполнения определяет событийный цикл (event loop).

console.log('1. Синхронный код');

setTimeout(() => console.log('4. Таймер'), 0);

Promise.resolve().then(() => console.log('3. Промис'));

console.log('2. Синхронный код');

Числа в строках показывают порядок, в котором строки появятся в консоли. Событийный цикл работает так:

  1. Сначала выполняется весь текущий синхронный код до конца.

  2. Затем выполняются все микрозадачи — колбэки промисов из .then, .catch, .finally и продолжения после await.

  3. Затем выполняется одна макрозадача — колбэк таймера, обработчик события, обработка ответа сети. После неё снова выполняются все накопившиеся микрозадачи, и цикл повторяется.

Поэтому таймер с задержкой 0 выполнился после промиса: задержка 0 означает «как можно скорее», но не раньше, чем закончится текущий код и все микрозадачи.

Из этого порядка следуют два практических вывода. Колбэк промиса никогда не выполнится раньше кода, который записан после вызова .then. А долгий синхронный код, например цикл на несколько секунд, блокирует всю очередь: пока он работает, страница не реагирует на нажатия и не перерисовывается.

Почитать ещё

async и await

Слова async и await позволяют работать с промисами без цепочек .then. Асинхронный код с ними читается сверху вниз, как обычный.

async function loadProduct(id) {
  const response = await fetch(`https://dummyjson.com/products/${id}?select=title,price`);
  const product = await response.json();
  console.log(`${product.title}: $${product.price}`);
  return product;
}

loadProduct(1);
console.log('Функция запущена');
  • Слово async перед функцией делает её асинхронной.

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

  • await можно использовать только внутри асинхронной функции.

Асинхронная функция всегда возвращает промис. Значение из return становится значением этого промиса. Поэтому вызывающий код получает результат через await или .then:

async function getPrice() {
  return 89990;
}

const result = getPrice();
console.log(result instanceof Promise);

getPrice().then((price) => console.log(price));

Строка «Функция запущена» в первом примере появилась раньше данных товара. Внутри loadProduct код ждёт ответа на await, но снаружи функция сразу вернула промис, и выполнение продолжилось.

Цепочку .then из прошлых разделов и код с await можно свободно заменять друг на друга, потому что оба варианта работают с одними и теми же промисами. Дальше в курсе мы пишем сетевой код через async и await.

Почитать ещё

Обработка ошибок: try, catch и finally

Если промис под await отклонён, в этом месте выбрасывается ошибка. Ошибку перехватывают конструкцией try...catch.

async function loadProduct(url) {
  console.log('Загружаем', url);
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Сервер ответил статусом ${response.status}`);
    }
    const product = await response.json();
    console.log('Готово:', product.title);
  } catch (error) {
    console.log('Ошибка:', error.message);
  } finally {
    console.log('Загрузка завершена');
  }
}

await loadProduct('https://dummyjson.com/products/1?select=title');
await loadProduct('https://dummyjson.com/products/99999');
await loadProduct('https://dummyjson.com/http/500');
  • Блок try содержит код, в котором может произойти ошибка.

  • Если в try выброшена ошибка, выполнение сразу переходит в блок catch. Переменная в скобках получает объект ошибки, а его свойство message — текст.

  • Блок finally выполняется всегда: и после успешного try, и после catch.

Адрес /http/500 у DummyJSON всегда отвечает статусом 500: так удобно проверять обработку ошибок сервера.

В этом примере await стоит вне функции, прямо в начале строки. Так можно писать в модулях, и в примерах курса мы пользуемся этим, чтобы запросы выполнялись по очереди. В обычном скрипте на странице await работает только внутри асинхронной функции.

В catch попадают три вида проблем: сеть недоступна и fetch отклонился, сервер ответил не 2xx и код сам выбросил ошибку, тело ответа не удалось разобрать. Пустой блок catch {} скрывает ошибку и от пользователя, и от разработчика, поэтому в нём всегда что-то делают: показывают сообщение или хотя бы выводят ошибку в консоль через console.error.

Почитать ещё

Одна функция для всех запросов

У каждого запроса к API повторяются одни и те же шаги: подставить адрес сервера, проверить статус, разобрать тело, превратить ошибку в понятный текст. Эти шаги выносят в одну функцию, и все остальные запросы вызывают её.

const API_URL = 'https://dummyjson.com';

async function request(path, options = {}) {
  let response;
  try {
    response = await fetch(API_URL + path, options);
  } catch {
    throw new Error('Сервер не отвечает. Проверьте подключение к интернету.');
  }

  if (response.status === 204) {
    return undefined;
  }

  const data = await response.json().catch(() => null);
  if (!response.ok) {
    throw new Error(data?.message ?? `Ошибка ${response.status}`);
  }
  return data;
}

async function show(path) {
  try {
    const data = await request(path);
    console.log('Успех:', data.title ?? data);
  } catch (error) {
    console.log('Ошибка:', error.message);
  }
}

await show('/products/1?select=title');
await show('/products/99999');

Разберём, что делает request:

  • Если fetch отклонился, ответ от сервера не получен. Функция выбрасывает ошибку с текстом, который можно показать пользователю.

  • Статус 204 означает успешный ответ без тела. Вызов response.json() на пустом теле выбросил бы ошибку, поэтому функция сразу возвращает undefined.

  • response.json().catch(() => null) пытается разобрать тело, а если оно не JSON, возвращает null вместо ошибки.

  • Если статус не из группы 2xx, функция выбрасывает ошибку. Текст берётся из поля message в теле ответа, а если его нет, составляется из статуса.

Теперь остальной код приложения короткий: const products = await request('/products'). Ошибки любого запроса приходят в одном формате, и показать их пользователю можно одинаково. Во втором модуле в проекте ITAM Board будет такая же функция, только на TypeScript.

Почитать ещё

Состояния загрузки

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

<label><input id="broken" type="checkbox" /> Имитировать ошибку сервера</label>
<div id="status"></div>
<ul id="products"></ul>

<style>
  .loader { color: #57606a; }
  .error { color: #cf222e; }
</style>

<script type="module">
  const statusBox = document.querySelector('#status');
  const list = document.querySelector('#products');
  const broken = document.querySelector('#broken');

  let products = [];
  let status = 'idle';
  let error = null;

  function render() {
    broken.disabled = status === 'loading';

    if (status === 'loading') {
      statusBox.innerHTML = '<p class="loader">Загружаем товары…</p>';
    } else if (status === 'error') {
      statusBox.innerHTML = `<p class="error">${error}</p><button id="retry" type="button">Повторить</button>`;
    } else {
      statusBox.innerHTML = '';
    }

    list.innerHTML = status === 'ready'
      ? products.map((product) => `<li>${product.title}</li>`).join('')
      : '';
  }

  async function loadProducts() {
    status = 'loading';
    error = null;
    render();

    try {
      const path = broken.checked ? '/http/500' : '/products?limit=5&select=title&delay=1500';
      const response = await fetch(`https://dummyjson.com${path}`);
      if (!response.ok) {
        throw new Error(`Не удалось загрузить товары: ошибка ${response.status}`);
      }
      const data = await response.json();
      products = data.products;
      status = 'ready';
    } catch (err) {
      status = 'error';
      error = err.message;
    } finally {
      render();
    }
  }

  statusBox.addEventListener('click', (event) => {
    if (event.target.closest('#retry')) {
      loadProducts();
    }
  });

  broken.addEventListener('change', () => loadProducts());

  loadProducts();
</script>

Товары загружаются с задержкой в полторы секунды, чтобы индикатор загрузки было видно. Включите флажок «Имитировать ошибку сервера»: товары загрузятся заново, и вместо списка появится ошибка. Нажмите «Повторить», затем снимите флажок, и товары загрузятся снова.

Статус принимает одно из четырёх значений:

  • idle — загрузку ещё не начинали;

  • loading — запрос отправлен, ответа ещё нет;

  • ready — данные загружены;

  • error — загрузить не удалось, в переменной error лежит текст ошибки.

Функция loadProducts меняет статус перед запросом и после ответа, а функция рендера показывает то, что соответствует статусу: индикатор, сообщение об ошибке с кнопкой повтора или список. Вызов рендера стоит в finally, поэтому страница обновится и после успеха, и после ошибки. Кнопка «Повторить» просто вызывает loadProducts ещё раз. Флажок при переключении тоже вызывает loadProducts, а на время загрузки отключается, чтобы новый запрос не начался раньше, чем закончится предыдущий.

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

Почитать ещё

  • Дока: async/await

Блокировка кнопки во время отправки

Когда пользователь отправляет форму, запрос тоже выполняется не мгновенно. Если в это время кнопка остаётся активной, пользователь может нажать её ещё раз и создать два одинаковых товара. Поэтому на время запроса кнопку отключают.

<form id="add-form">
  <label for="title">Название товара</label>
  <input id="title" name="title" type="text" required />
  <button type="submit">Добавить</button>
  <p id="form-error" class="error"></p>
</form>
<ul id="products"></ul>

<style>
  .error { color: #cf222e; }
</style>

<script type="module">
  const form = document.querySelector('#add-form');
  const button = form.querySelector('button');
  const formError = document.querySelector('#form-error');
  const list = document.querySelector('#products');

  form.addEventListener('submit', async (event) => {
    event.preventDefault();
    formError.textContent = '';
    button.disabled = true;
    button.textContent = 'Добавляем…';

    try {
      const response = await fetch('https://dummyjson.com/products/add?delay=1500', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ title: form.elements.title.value.trim() }),
      });
      if (!response.ok) {
        throw new Error(`Не удалось добавить товар: ошибка ${response.status}`);
      }
      const created = await response.json();

      const item = document.createElement('li');
      item.textContent = `${created.title} (id ${created.id})`;
      list.append(item);
      form.reset();
    } catch (error) {
      formError.textContent = error.message;
    } finally {
      button.disabled = false;
      button.textContent = 'Добавить';
    }
  });
</script>

Введите название и нажмите «Добавить». Пока запрос выполняется, кнопка отключена, а её текст сообщает, что происходит.

  • Перед запросом кнопке задают disabled = true. Отключённую кнопку нельзя нажать, и форма не отправится повторно, даже по Enter.

  • В finally кнопку включают обратно. Если бы это стояло только в try, после ошибки кнопка осталась бы отключённой навсегда.

  • Ошибку показывают под формой, рядом с кнопкой, которую нажал пользователь. Перед новой отправкой старое сообщение об ошибке стирают.

  • Форму очищают только после успешного ответа. Если сервер ответил ошибкой, введённые данные остаются в полях, и пользователю не придётся вводить их заново.

Почитать ещё

  • Дока: disabled

Частые ошибки с async и await

Разберём ошибки, которые чаще всего встречаются в асинхронном коде.

Забыли await. Без await в переменную попадает промис, а не результат:

async function getProducts() {
  const response = await fetch('https://dummyjson.com/products?limit=2&select=title');
  return response.json();
}

const withoutAwait = getProducts();
console.log(withoutAwait.products);

const withAwait = await getProducts();
console.log(withAwait.products);

await внутри forEach. Метод forEach не ждёт асинхронные колбэки. Код после цикла выполнится раньше, чем закончатся запросы:

const ids = [1, 2, 3];
const titles = [];

ids.forEach(async (id) => {
  const response = await fetch(`https://dummyjson.com/products/${id}?select=title`);
  const product = await response.json();
  titles.push(product.title);
});

console.log('После forEach:', titles.length);

for (const id of ids) {
  const response = await fetch(`https://dummyjson.com/products/${id}?select=title`);
  const product = await response.json();
  titles.push(product.title);
}

console.log('После for...of:', titles.length);

Если запросы нужно выполнить по очереди, используйте цикл for...of с await. Если одновременно — await Promise.all(ids.map(...)).

Независимые запросы по очереди. Каждый await ждёт завершения предыдущего запроса, и общее время складывается:

const load = (path) => fetch(`https://dummyjson.com${path}`).then((response) => response.json());

let started = Date.now();
await load('/products/1?delay=700');
await load('/products/2?delay=700');
console.log(`По очереди: ${Date.now() - started} мс`);

started = Date.now();
await Promise.all([load('/products/1?delay=700'), load('/products/2?delay=700')]);
console.log(`Одновременно: ${Date.now() - started} мс`);

Промис без обработки ошибки. Если асинхронную функцию вызвать без await и без .catch, её ошибка появится в консоли как «Uncaught (in promise)», а пользователь не узнает о проблеме. У каждого вызова асинхронной функции должна быть обработка ошибки: try...catch внутри функции или у вызывающего кода.

Почитать ещё

Задания