async/await и fetch
Зачем это нужно
Промисы решили «ад колбэков», но цепочки then всё равно читаются хуже обычного кода. Ключевые
слова async и await позволяют писать асинхронный код так, будто он синхронный: сверху вниз,
с обычными переменными и обычным try … catch.
А fetch — это то, ради чего всё затевалось: запросы к серверу. Каждый сайт, который показывает
погоду, ленту новостей, товары или сообщения, получает их через запросы. В React данные
загружаются ровно так: useEffect + fetch + await.
async-функция
Слово async перед функцией делает две вещи:
- функция всегда возвращает промис:
return 5превращается в промис со значением5; - внутри можно писать
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, в браузере выдаёт
ошибку.
Частые ошибки
- Забытый
await.const user = fetch(url);— вuserпромис, а не ответ. Аresponse.json()безawait— промис, а не данные. awaitвнеasync-функции. В обычной функцииawait— синтаксическая ошибка. (В модулях и в задачах этого курсаawaitразрешён и на верхнем уровне.)- Нет проверки
response.ok. Ответ 404 не попадёт вcatchсам по себе. - Лишняя последовательность. Независимые запросы через
awaitподряд — медленно. ИспользуйPromise.all. forEachсasync. Не ждёт. Используйfor … ofилиPromise.allсmap.- Забыли
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) }) |