Урок 10. Асинхронность: setTimeout, промисы, async/await и fetch
Некоторые действия занимают время: подождать секунду, скачать данные с сервера, прочитать большой файл. Если бы программа на это время замирала, страница перестала бы отвечать на нажатия. Поэтому в JavaScript такие действия асинхронные: программа запускает действие, идёт дальше, а результат обрабатывает потом, когда он будет готов.
setTimeout: выполнить позже
setTimeout(функция, задержка) вызывает функцию через указанное число миллисекунд (1000 мс — одна секунда):
console.log("Первый");
setTimeout(() => {
console.log("Третий — через секунду");
}, 1000);
console.log("Второй");Первый
Второй
Третий — через секунду«Второй» выводится раньше «Третьего», хотя стоит ниже в коде: setTimeout не ждёт, а только ставит функцию в очередь и сразу возвращает управление. Даже с задержкой 0 функция выполнится только после того, как закончится весь текущий код.
Так устроен цикл событий (event loop): JavaScript выполняет код в одном потоке, по одной задаче. Таймеры, ответы сервера и щелчки мыши встают в очередь, и их обработчики запускаются по одному, когда поток свободен.
setTimeout возвращает номер таймера; clearTimeout(номер) отменяет вызов, если он ещё не случился. Повторять действие через равные промежутки умеет setInterval — о нём в справке Как сделать таймер.
Промисы
Колбэки удобны для одного действия, но когда действий несколько и каждое ждёт предыдущего, код уходит вложенными функциями вправо и плохо читается. Поэтому появились промисы (Promise — «обещание»).
Промис — объект, который представляет результат, которого ещё нет. У него три состояния:
- ожидание (pending) — действие идёт;
- выполнен (fulfilled) — есть результат;
- отклонён (rejected) — произошла ошибка.
Многие функции браузера и Node.js сразу возвращают промис — например, fetch. Свой промис создают так:
function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
wait(500).then(() => {
console.log("Прошло полсекунды");
});
console.log("Ожидание…");Ожидание…
Прошло полсекундыnew Promise получает функцию с параметрами resolve и reject. Вызов resolve(значение) выполняет промис, reject(ошибка) — отклоняет. Метод then назначает функцию, которая получит результат, catch — функцию для ошибки, finally — функцию, которая выполнится в любом случае.
function divide(a, b) {
return new Promise((resolve, reject) => {
if (b === 0) {
reject(new Error("делить на ноль нельзя"));
} else {
resolve(a / b);
}
});
}
divide(1, 0)
.then((result) => console.log("Результат:", result))
.catch((err) => console.log("Ошибка:", err.message))
.finally(() => console.log("Готово"));Ошибка: делить на ноль нельзя
Готовоnew Error("текст") создаёт объект ошибки; текст из него читают через err.message.
async и await
Цепочки then читаются лучше колбэков, но ещё проще запись через async и await. Она выглядит как обычный последовательный код:
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function main() {
console.log("Старт");
await wait(300);
console.log("Прошло 0,3 секунды");
await wait(300);
console.log("Прошло ещё 0,3 секунды");
}
main();Старт
Прошло 0,3 секунды
Прошло ещё 0,3 секундыasyncперед функцией делает её асинхронной: она всегда возвращает промис.awaitставится перед промисом и ждёт его результата. Пока функция ждёт, остальная программа и страница продолжают работать.awaitможно писать только внутриasync-функции и ещё на верхнем уровне модулей (о модулях — в справке Как подключить JavaScript к HTML). Поэтому код часто собирают вasync function main()и вызывают её.
Ошибки: try и catch
Ошибка в обычном коде останавливает программу. Чтобы перехватить её и обработать, код ставят в блок try, а обработку — в блок catch:
try {
const data = JSON.parse("не JSON");
console.log(data);
} catch (err) {
console.log("Ошибка разбора:", err.name);
}
console.log("Программа продолжает работу");Ошибка разбора: SyntaxError
Программа продолжает работуЕсли в блоке try случилась ошибка, остаток блока пропускается, а выполняется catch; в его параметре — объект ошибки. С await всё так же: отклонённый промис внутри try попадает в catch.
function failLater() {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("сервер не ответил")), 100);
});
}
async function main() {
try {
await failLater();
console.log("Сюда не дойдёт");
} catch (err) {
console.log("Поймано:", err.message);
}
}
main();Поймано: сервер не ответилНесколько действий сразу: Promise.all
Если действия не зависят друг от друга, их запускают одновременно, а ждут все вместе — через Promise.all:
function delay(value, ms) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
async function main() {
const results = await Promise.all([
delay("А", 300),
delay("Б", 100),
delay("В", 200),
]);
console.log(results);
}
main();[ 'А', 'Б', 'В' ]Все три таймера идут одновременно, поэтому ждать приходится 0,3 секунды, а не 0,6. Результаты стоят в том же порядке, что и промисы, — неважно, какой закончился первым. Если отклонится хотя бы один промис, Promise.all тоже отклоняется.
fetch: запрос к серверу
fetch(адрес) отправляет HTTP-запрос и возвращает промис с ответом. Он есть во всех современных браузерах, а в Node.js — начиная с версии 18, без установки пакетов. Вот программа для Node.js: она скачивает с сайта Node.js список его версий в формате JSON и выводит самую свежую версию LTS (с долгой поддержкой).
async function main() {
const response = await fetch("https://nodejs.org/dist/index.json");
if (!response.ok) {
throw new Error("Сервер ответил кодом " + response.status);
}
const versions = await response.json();
const lts = versions.find((v) => v.lts);
console.log("Свежая LTS:", lts.version, lts.lts);
}
main().catch((err) => console.log("Ошибка:", err.message));Программа выводит строку вида Свежая LTS: v… … — номер версии и её кодовое имя. Без интернета будет Ошибка: fetch failed.
Порядок работы с fetch:
await fetch(адрес)— дождаться ответа. В нём уже есть код ответа сервера, но тело ещё не прочитано.- Проверить
response.ok—true, если код ответа от 200 до 299.fetchне считает ошибкой ответы 404 или 500: промис выполняется, и толькоresponse.okпокажет, что что-то не так. Отклоняется он лишь тогда, когда ответа нет совсем: нет сети, неверный адрес сервера. await response.json()— прочитать тело и разобрать JSON. Для простого текста —response.text().
throw new Error(...) выбрасывает свою ошибку: дальше код не идёт, а ошибка попадает в ближайший catch.
fetch в браузере
На странице тот же код выводит данные в элементы. Пусть рядом со страницей лежит файл data.json с массивом товаров вида [{"name": "Хлеб", "price": 2.1}]:
async function loadProducts() {
const list = document.querySelector("#products");
try {
const response = await fetch("data.json");
if (!response.ok) {
throw new Error("код ответа " + response.status);
}
const products = await response.json();
for (const p of products) {
const item = document.createElement("li");
item.textContent = `${p.name} — ${p.price} руб.`;
list.append(item);
}
} catch (err) {
list.textContent = "Не удалось загрузить товары: " + err.message;
}
}
loadProducts();Две частые причины, по которым такой код не работает:
- Страница открыта двойным щелчком. Адрес в браузере начинается с
file://, аfetchс такой страницы браузеры обычно не выполняют. Нужен локальный веб-сервер: например, командаnpx http-serverв папке со страницей (нужен Node.js) илиpython3 -m http.server(нужен Python; в Windows —python -m http.server). Команда покажет адрес видаhttp://localhost:…— по нему и откройте страницу. - Запрос к чужому сайту. Браузер разрешает читать ответ другого сайта, только если тот явно это позволил (правила CORS). Иначе в консоли — ошибка со словами
CORS policy. Исправляется это на стороне того сервера, а не в коде страницы. В Node.js таких ограничений нет.
Коротко
- Асинхронный код не ждёт:
setTimeoutиfetchставят работу в очередь, программа идёт дальше. - Промис — будущий результат:
then— успех,catch— ошибка,finally— в любом случае. async function+await— ожидание промиса записью обычного кода; ошибки — вtry … catch.Promise.allждёт несколько промисов сразу, результаты — в исходном порядке.fetch: проверьтеresponse.ok, потомawait response.json(); 404 — не ошибка дляfetch.