Перейти к содержимому
Тема 19 из 21
Теория

async/await и fetch

Зачем это нужно

Промисы решили «ад колбэков», но цепочки then всё равно читаются хуже обычного кода. Ключевые слова async и await позволяют писать асинхронный код так, будто он синхронный: сверху вниз, с обычными переменными и обычным try … catch.

А fetch — это то, ради чего всё затевалось: запросы к серверу. Каждый сайт, который показывает погоду, ленту новостей, товары или сообщения, получает их через запросы. В React данные загружаются ровно так: useEffect + fetch + await.

async-функция

Слово async перед функцией делает две вещи:

  1. функция всегда возвращает промис: return 5 превращается в промис со значением 5;
  2. внутри можно писать await.
async function getNumber() {
  return 5;
}

const p = getNumber();
console.log(p instanceof Promise);
p.then((value) => console.log(value));

Вывод:

true
5

Стрелочные функции тоже бывают асинхронными: const load = async (id) => { ... };

await — дождаться промиса

await промис приостанавливает эту функцию, пока промис не выполнится, и возвращает его значение:

const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));

async function main() {
  console.log("Начали");
  const name = await wait(100, "Аня");          // ждём 100 мс
  const city = await wait(100, "Душанбе");      // ещё 100 мс
  console.log(name + " из города " + city);
}

main();
console.log("main запущена, но ещё не закончилась");

Вывод:

Начали
main запущена, но ещё не закончилась
Аня из города Душанбе

Важно: await не останавливает всю программу. Он ставит на паузу только свою async-функцию, а остальной код идёт дальше. Поэтому вторая строка напечаталась раньше третьей.

Это тот же код, что цепочка then, просто записан иначе: всё после await — как будто внутри then.

Ошибки: try … catch

Если промис под await отклонён, await выбрасывает ошибку, как throw. Её ловят обычным try … catch:

const fail = () => new Promise((_, reject) => setTimeout(() => reject(new Error("сервер недоступен")), 50));

async function main() {
  try {
    await fail();
    console.log("сюда не дойдём");
  } catch (err) {
    console.log("Ошибка: " + err.message);
  } finally {
    console.log("Загрузка закончена");
  }
}

main();

Вывод:

Ошибка: сервер недоступен
Загрузка закончена

По очереди или параллельно

Два await подряд выполняются по очереди: второй запрос начнётся, только когда закончится первый. Если запросы не зависят друг от друга, это лишнее ожидание. Запусти их сразу и жди вместе через Promise.all:

const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));

async function sequential() {
  const a = await wait(100, "A");
  const b = await wait(100, "B");
  return [a, b];
}

async function parallel() {
  const [a, b] = await Promise.all([wait(100, "A"), wait(100, "B")]);
  return [a, b];
}

async function main() {
  let start = Date.now();
  await sequential();
  console.log("По очереди дольше 190 мс: " + (Date.now() - start >= 190));
  start = Date.now();
  await parallel();
  console.log("Параллельно меньше 190 мс: " + (Date.now() - start < 190));
}

main();

Вывод:

По очереди дольше 190 мс: true
Параллельно меньше 190 мс: true

Правило: если следующему шагу нужен результат предыдущего — await по очереди. Если нет — Promise.all.

await в цикле

for (const id of ids) {
  const user = await loadUser(id);   // по очереди: каждый ждёт предыдущего
}

const users = await Promise.all(ids.map((id) => loadUser(id)));   // все сразу

А вот ids.forEach(async (id) => { await ... }) — ловушка: forEach не ждёт асинхронные колбэки, и код после него выполнится раньше, чем загрузки закончатся.

fetch — запрос к серверу

fetch(адрес) отправляет HTTP-запрос и возвращает промис ответа (Response). Тело ответа читается отдельно и тоже асинхронно — поэтому await два раза:

async function loadTodo() {
  const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  console.log(response.status);       // 200 — код ответа
  console.log(response.ok);           // true, если код 200–299
  const todo = await response.json(); // тело ответа: JSON → объект
  console.log(todo.title);
}

loadTodo();

Примерно так выглядит результат (это настоящий учебный сервер):

200
true
delectus aut autem

Что есть у ответа:

Свойство / метод Что даёт
response.status код ответа: 200, 201, 404, 500…
response.ok true, если код от 200 до 299
await response.json() тело, разобранное из JSON
await response.text() тело как строка
response.headers.get("Content-Type") заголовок ответа

Коды, которые встречаются чаще всего: 200 — всё хорошо, 201 — создано, 400 — неправильный запрос, 401 / 403 — нет доступа, 404 — не найдено, 500 — ошибка на сервере.

Ошибка сети и ошибка сервера — разные вещи

fetch отклоняет промис только когда ответа нет совсем: нет интернета, сервер недоступен, неверный адрес. Ответ 404 или 500 — это успешно полученный ответ с плохим кодом. Его нужно проверять самому:

async function loadJSON(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json();
}

Без проверки response.ok программа попытается разобрать страницу ошибки как данные.

Отправить данные: POST

Чтобы отправить данные (создать задачу, войти, оставить комментарий), вторым аргументом передают настройки запроса:

async function addComment(text) {
  const response = await fetch("https://api.example.com/comments", {
    method: "POST",                                    // по умолчанию GET
    headers: { "Content-Type": "application/json" },   // говорим серверу: в теле JSON
    body: JSON.stringify({ text }),                    // тело — строка
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();   // сервер обычно присылает созданный объект
}

Основные методы HTTP: GET — получить, POST — создать, PUT / PATCH — изменить, DELETE — удалить. Вместе они образуют REST API — так устроено большинство серверов, с которыми ты будешь работать.

Типичная загрузка данных

Вот шаблон, который встречается в каждом приложении — и в React в том числе. У загрузки три состояния: идёт, получилось, ошибка:

async function showUsers() {
  console.log("Загрузка...");
  try {
    const users = await loadJSON("https://jsonplaceholder.typicode.com/users");
    console.log("Пользователей: " + users.length);
  } catch (err) {
    console.log("Не удалось загрузить: " + err.message);
  } finally {
    console.log("Скрываем индикатор загрузки");
  }
}

В React эти три состояния станут тремя переменными состояния: loading, data и error.

Как это работает внутри

async/await — это «синтаксический сахар» над промисами. Встретив await, движок запоминает, где остановилась функция, и выходит из неё, возвращая промис. Остальной код продолжает работать. Когда ожидаемый промис выполнится, продолжение функции ставится в очередь микрозадач и выполняется с того же места, со всеми теми же переменными.

fetch выполняет сам запрос силами браузера или Node.js — вне JavaScript. В браузере есть ограничение CORS: страница может читать ответы чужого сервера, только если тот это разрешил специальными заголовками. Поэтому иногда запрос, который работает в Node.js, в браузере выдаёт ошибку.

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

  1. Забытый await. const user = fetch(url); — в user промис, а не ответ. А response.json() без await — промис, а не данные.
  2. await вне async-функции. В обычной функции await — синтаксическая ошибка. (В модулях и в задачах этого курса await разрешён и на верхнем уровне.)
  3. Нет проверки response.ok. Ответ 404 не попадёт в catch сам по себе.
  4. Лишняя последовательность. Независимые запросы через await подряд — медленно. Используй Promise.all.
  5. forEach с async. Не ждёт. Используй for … of или Promise.all с map.
  6. Забыли JSON.stringify для body. Передать объект напрямую нельзя — в теле только строка.

Шпаргалка

Что Как
Асинхронная функция async function f() { ... }, async () => { ... }
Дождаться const x = await promise;
Ошибка try { await ... } catch (err) { ... }
Параллельно const [a, b] = await Promise.all([p1, p2]);
GET const res = await fetch(url); const data = await res.json();
Проверка if (!res.ok) throw new Error(HTTP ${res.status});
POST fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(obj) })

Имя пользователя

Лёгкая+10 XP

Напиши async-функцию getUserName(id). Она делает запрос fetch по адресу https://api.example.com/users/<id>, разбирает ответ как JSON (это объект { id, name }) и возвращает name.

await getUserName(1)   → "Ann"
await getUserName(2)   → "Timur"

Подключи коннектор, чтобы решать задачи

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

Ищем коннектор на этом компьютере…

  1. 1
    Скачай коннектор для своей системы (Windows, macOS или Linux).Скачать коннекторНа странице загрузки выбери файл со своей системой в названии, например windows-amd64.
  2. 2Запусти его и выбери папку, где будут лежать твои решения. Python и Node.js он поставит туда сам, когда понадобится.
  3. 3
    Введи ниже код из 6 цифр — он написан в окне коннектора («Код для сайта CodeWithMe»). Если браузер спросит про доступ к локальной сети — разреши.

Коннектор открытый (исходный код на GitHub), работает только на твоём компьютере и выполняет код лишь после ввода кода сопряжения.