Когда вы открываете сайт, он не замирает, пока загружаются картинки, курс валют или список товаров. Страница живёт, а данные «приезжают» позже. Это асинхронность: JavaScript умеет не ждать у плиты, а заниматься другими делами, пока готовится ответ. Сегодня разберём, как это устроено, и научимся писать такой код без путаницы.
Объяснять разницу между синхронным и асинхронным кодом
Откладывать действия через setTimeout и повторять через setInterval
Читать код с колбэками и понимать, чем плох «ад колбэков»
Создавать промисы и строить цепочки then / catch / finally
Писать async / await с try / catch
Предсказывать порядок вывода: стек, микрозадачи, макрозадачи (event loop)
Как работать с этой страницей
Весь код на тёмном фоне с кнопкой ▶ Запустить — живой. Его можно менять и сразу видеть результат в консоли. В этом уроке код часто что-то выводит с задержкой — подождите секунду, пока консоль доберёт строки. Много задач вида «предскажи порядок вывода»: сначала думайте и записывайте прогноз, потом запускайте. Подсказки спрятаны под замочком 🔒 — открывайте, только если застряли.
01 Синхронно и асинхронно. setTimeout
До сегодняшнего дня весь наш код был синхронным: JavaScript выполнял строки строго по порядку, одну за другой, и следующая не начиналась, пока не закончилась предыдущая.
Но в реальных программах есть дела, которые занимают время: запрос на сервер, загрузка картинки, таймер, ответ пользователя. Если ждать каждое из них, пока оно закончится, страница «зависнет». Поэтому такие дела запускают и не ждут: когда результат будет готов, JavaScript вернётся к нему. Это и есть асинхронный код.
Аналогия из жизни
Вы пришли в чайхону и заказали плов. Официант не стоит рядом с котлом, пока рис не дойдёт, — он принимает заказы у других столов. Когда плов готов, повар зовёт официанта, и тот несёт блюдо вам. Синхронный официант стоял бы у котла 40 минут и не обслужил никого. Асинхронный — запустил готовку и пошёл дальше.
setTimeout — «сделай это позже»
Самый простой способ получить асинхронность — функция setTimeout(функция, миллисекунды). Она говорит браузеру: «через столько-то миллисекунд вызови эту функцию», а сама сразу возвращается. 1000 мс = 1 секунда.
Код после setTimeout не ждёт · Ctrl + Enter
console.log("Заказали плов");
setTimeout(() => {
console.log("Плов готов!");
}, 500);
console.log("Официант идёт к другому столу");
Обратите внимание на порядок: «Официант идёт…» появилось раньше «Плов готов!», хотя написано ниже. Функцию внутри setTimeout мы передаём, а не вызываем: она сработает потом. Такая функция называется колбэком (callback — «перезвони мне») — подробнее о них в следующем разделе.
Нулевая задержка тоже асинхронна
Самый частый сюрприз: даже setTimeout(…, 0) не выполняет функцию сразу. Он ставит её в очередь на «после того, как закончится весь текущий код».
Можно редактировать · Ctrl + Enter
console.log("A");
setTimeout(() => console.log("B (с нулевой задержкой)"), 0);
console.log("C");
A — обычная строка, выводится сразу.
setTimeout — функция «отложена» в сторону. Дальше JavaScript идёт, не оглядываясь.
C — выводится сразу.
Весь код закончился, и только теперь JavaScript достаёт отложенную функцию и выводит B.
Аргументы и отмена
setTimeout возвращает число — номер таймера. По нему таймер можно отменить через clearTimeout, пока он не сработал.
Можно редактировать · Ctrl + Enter
const timerId = setTimeout(() => console.log("Это не выведется"), 300);
console.log("Таймер запущен, номер есть:", typeof timerId);
clearTimeout(timerId);
console.log("Таймер отменили");
setInterval — «повторяй каждые N мс»
setInterval работает так же, но вызывает функцию снова и снова, пока вы его не остановите через clearInterval. Если забыть остановить — таймер будет тикать вечно (в этой странице выполнение прервётся через 8 секунд).
Обратный отсчёт · Ctrl + Enter
let left = 3;
const id = setInterval(() => {
console.log("Осталось:", left);
left--;
if (left ===0) {
clearInterval(id); // останавливаем, иначе будет тикать вечно
console.log("Поехали!");
}
}, 200);
Частая ошибка
Вызвать функцию вместо того, чтобы передать её. setTimeout(sayHi(), 1000) — скобки запускают функцию прямо сейчас, а в таймер уходит то, что она вернула (то есть ничего). Нужно setTimeout(sayHi, 1000) или setTimeout(() => sayHi(), 1000).
Ошибка: таймер сработал сразу · Ctrl + Enter
functionsayHi() {
console.log("Салом!");
}
setTimeout(sayHi(), 1000); // ✖ «Салом!» выведется мгновенно, а не через секунду
console.log("Конец кода");
Большинство задач этого урока — про порядок, в котором строки попадут в консоль. В задачах «предскажи порядок» код сам сверяет ваш прогноз с реальностью: впишите ответ в массив prediction и запустите. Увидите «Верно!» — вы мыслите как JavaScript.
ЗАДАЧА 1.1
Заказ в чайхоне
лёгкая
Закажите плов: между двумя строками добавьте таймер, который через 500 мс выведет «Плов готов!». Официант не должен ждать — строка про другой стол должна появиться раньше.
Можно редактировать · Ctrl + Enter
console.log("Заказ принят");
// здесь запустите таймер на 500 мс
console.log("Официант идёт к другому столу");
Ожидаемый результат
Заказ принят
Официант идёт к другому столу
Плов готов!
Подсказка 1
Нужен setTimeout с двумя аргументами: функция и число миллисекунд.
Подсказка 2
setTimeout(() => { console.log("Плов готов!"); }, 500); — и пусть строка стоит между двумя console.log.
ЗАДАЧА 1.2
Предскажи порядок: три таймера
лёгкая
Функция log запоминает метки в порядке вывода. Не запуская код, впишите в prediction порядок меток, в котором они появятся (например, ["1", "2", "3", "4"]). Затем запустите — проверка в конце скажет, угадали ли вы.
Скобки у console.log(...) вызывают функцию немедленно. В setTimeout нужно передать функцию, а не результат её вызова.
Подсказка 2
Оберните вызов в стрелочную функцию: () => console.log("Таймер сработал").
02 Callback: «перезвони, когда будет готово»
Мы уже передавали функции в map и filter. Функция, которую передают в другую функцию «на потом», называется колбэком. Раньше колбэки вызывались сразу, внутри цикла. А в асинхронном коде колбэк вызывается, когда работа закончится.
Функция сообщает о результате через колбэк · Ctrl + Enter
Вы заказываете торт в кондитерской и оставляете свой номер: «Позвоните, когда будет готов». Номер — это колбэк. Вы идёте по делам, а кондитер сам позвонит. Вам не нужно стоять у витрины и спрашивать каждые пять минут.
Колбэк с ошибкой: договорённость «error-first»
Асинхронная работа может не получиться: нет сети, нет такого товара, неверный пароль. Поэтому по традиции первым аргументом колбэк получает ошибку (или null, если всё хорошо), а вторым — результат.
Колбэк (error, result) · Ctrl + Enter
functionfindStudent(id, callback) {
setTimeout(() => {
if (id !==1) {
callback(newError("студент не найден")); // ошибка — первый аргументreturn;
}
callback(null, "Мадина"); // успех — ошибки нет
}, 200);
}
findStudent(1, (error, name) => {
if (error) {
console.log("Ошибка:", error.message);
return;
}
console.log("Нашли:", name);
});
findStudent(7, (error, name) => {
if (error) {
console.log("Ошибка:", error.message);
return;
}
console.log("Нашли:", name);
});
Ад колбэков (callback hell)
Что, если шаги зависят друг от друга: сначала найти пользователя, потом его заказы, потом адрес доставки? Каждый следующий шаг приходится вкладывать в колбэк предыдущего. Получается «лесенка», которая уезжает вправо.
Всего четыре шага, а код уже трудно читать. А если добавить обработку ошибок на каждом уровне — получится настоящий лабиринт. Именно эту проблему решили промисы, о которых мы поговорим чуть позже: те же шаги выстраиваются в ровную цепочку.
Колбэки
Вложенность растёт вправо. Ошибки нужно проверять на каждом уровне вручную. Код сложно менять.
Промисы и async/await
Шаги идут друг за другом ровной колонкой. Ошибки ловит одно место — catch или try/catch.
Частая ошибка
Писать код после асинхронного вызова и ждать, что результат уже есть. Колбэк ещё не сработал, а строка ниже уже выполнилась.
Ошибка: результата ещё нет · Ctrl + Enter
let price;
setTimeout(() => {
price = 35;
}, 200);
console.log("Цена плова:", price); // ✖ undefined — таймер ещё не сработал
Всё, что зависит от результата, нужно делать внутри колбэка (или, как мы увидим дальше, после await).
Пишем свои асинхронные функции с колбэками и обрабатываем ошибки.
ЗАДАЧА 2.1
Меню с задержкой
лёгкая
Напишите функцию loadMenu(callback): через 200 мс она вызывает callback и передаёт ему массив ["самбуса", "курутоб", "чой"]. Вызов функции уже написан.
Функция checkAge готова и сообщает результат по правилу error-first. Вызовите её дважды — для возраста 20 и для 15 — и в колбэке выведите либо сообщение об успехе, либо Ошибка: … с текстом ошибки.
Колбэк принимает два параметра: (error, result). Сначала проверяем error.
Подсказка 2
(error, result) => { if (error) { console.log("Ошибка: " + error.message); return; } console.log(result); } — этот колбэк можно использовать в обоих вызовах.
ЗАДАЧА 2.3
Заказ плова по шагам
лёгкая
Три шага заказа уже написаны, каждый занимает 100 мс. Запустите их друг за другом: следующий шаг вызывается в колбэке предыдущего. Каждый шаг передаёт в колбэк строку — выведите её.
Начните с takeOrder((text) => { ... }). Внутри колбэка выведите text и вызовите следующий шаг.
Подсказка 2
Получится три уровня вложенности: takeOrder(…) содержит cook(…), а тот содержит serve(…). Это и есть маленькая «пирамида».
03 Как это работает: event loop
JavaScript однопоточный: у него одни руки, он делает одно дело за раз. Как же тогда таймеры и запросы «идут сами»? Ждёт их не JavaScript, а браузер. Когда таймер сработал, браузер кладёт ваш колбэк в очередь, а специальный механизм — event loop (цикл событий) — достаёт его оттуда, когда JavaScript освободится.
Аналогия из жизни
Представьте один кассовый аппарат в магазине на Шохмансуре. Кассир (стек вызовов) обслуживает одного покупателя за раз. Остальные стоят в очередях. Но очередей две: у «быстрой кассы» стоят те, кто с одной покупкой (микрозадачи — промисы), а в обычной очереди — все остальные (макрозадачи — таймеры). Кассир закончил с покупателем, и сначала обслуживает всю быструю кассу, и только потом берёт одного человека из обычной очереди.
Схема
Call StackСтек вызовов: что выполняется прямо сейчасmain()console.log()
Web API (браузер)Здесь «ждут» таймеры и запросы. JavaScript не занятsetTimeoutfetchклики
Очередь микрозадачБыстрая касса: идут первымиpromise.thenawaitqueueMicrotask
Очередь макрозадачОбычная очередь: по одной за кругколбэк setTimeoutsetIntervalсобытия
Event loop: стек пуст → выполнить ВСЕ микрозадачи → взять ОДНУ макрозадачу → снова
Сначала весь синхронный код, затем микрозадачи, затем по одной макрозадаче
Call Stack
Стек вызовов. Здесь работает JavaScript. Пока в стеке есть код, всё остальное ждёт.
Макрозадача
Колбэк таймера setTimeout / setInterval, обработчик клика и подобное. За один «круг» выполняется только одна.
Микрозадача
Колбэки .then / .catch / .finally, продолжение после await, queueMicrotask. После каждой макрозадачи выполняются все, пока очередь не опустеет.
Порядок: таймер 1, промис внутри таймера 1, таймер 2. Микрозадача, рождённая таймером 1, успела раньше, чем «обычная очередь» дошла до таймера 2.
Частая ошибка
Думать, что setTimeout(fn, 0) сработает «сразу». На самом деле он сработает только когда закончится весь текущий код и все микрозадачи. А если текущий код занят тяжёлым циклом, таймер будет ждать, пока цикл не завершится: setTimeout гарантирует «не раньше, чем через N мс», а не «ровно через N мс».
Совет: как предсказывать порядок
Идите по коду сверху вниз и раскладывайте строки по трём «корзинам»: 1. выполняется сразу, 2. в микрозадачи (then, await), 3. в макрозадачи (setTimeout; у кого задержка меньше — тот раньше). Потом читайте: корзина 1 → корзина 2 → корзина 3. Если внутри колбэка появилась новая корзина-2 или корзина-3, повторите то же самое.
Сначала F (синхронно), затем единственная микрозадача D. Она кладёт в очередь макрозадач ещё один таймер — E, в конец очереди, после A и C.
Подсказка 2
Очередь макрозадач после первого круга: A, C, E. Но после выполнения A сразу же срабатывает его микрозадача B — раньше, чем C.
ЗАДАЧА 3.5
Задержки и цепочка
сложная
Таймеры с разной задержкой и цепочка из двух then. Цепочка — это две микрозадачи подряд: вторая появляется, когда выполнилась первая, но всё равно раньше любого таймера.
Синхронно — только S. Затем все микрозадачи: P1 и следом P2.
Подсказка 2
Таймеры срабатывают по возрастанию задержки: 0, 100, 200 — независимо от того, в каком порядке они записаны в коде.
04 Promise: обещание результата
Промис (Promise — «обещание») — это объект, который представляет результат асинхронной работы, которого пока нет, но который будет: либо значение, либо ошибка. Вместо вложенных колбэков мы получаем объект, на который можно «подписаться» методами then и catch.
Аналогия из жизни
Вы оплатили доставку и получили чек с номером заказа. Это ещё не товар, но обещание: «товар будет». У обещания три состояния: ждём (заказ в пути), выполнено (курьер привёз) или отказ (товара нет на складе). Чек вы держите в руках сразу, а привезут позже — или отменят.
Три состояния промиса
pending
«Ожидает». Начальное состояние: результата ещё нет.
fulfilled
«Выполнен». Работа закончилась успешно, у промиса есть значение. Вызывается колбэк из then.
rejected
«Отклонён». Что-то пошло не так, у промиса есть причина (ошибка). Вызывается колбэк из catch.
Важно: из pending промис переходит в другое состояние один раз и навсегда. Выполненный промис нельзя «отклонить» позже.
Создание промиса
Промис создают через new Promise. В конструктор передают функцию с двумя параметрами: resolve (вызвать при успехе) и reject (вызвать при ошибке).
Промис, который выполнится через 300 мс · Ctrl + Enter
Функция wait здесь — полезная заготовка. Она возвращает промис, который выполняется через заданное время. Мы будем использовать её в уроке постоянно.
Цепочки промисов
Главная сила промисов: thenвозвращает новый промис. Поэтому then можно соединять в цепочку, а значение, которое вернул один then, попадает в следующий.
Каждый then получает результат предыдущего · Ctrl + Enter
Promise.resolve(5)
.then((n) => {
console.log("Получили:", n);
return n * 2; // это значение уйдёт дальше
})
.then((n) => {
console.log("Удвоили:", n);
return n + 1;
})
.then((n) => {
console.log("Прибавили 1:", n);
});
Если колбэк then возвращает промис, цепочка подождёт его завершения и только потом пойдёт дальше. Так выстраиваются последовательные шаги, и пирамида колбэков из прошлого раздела превращается в ровную колонку:
Если в любом звене случилась ошибка (reject или throw), цепочка пропускает все следующие then и «проваливается» в ближайший catch. Один catch в конце ловит ошибки всех шагов. А если catch вернул значение — цепочка снова считается успешной и идёт дальше.
Если в колбэке then нет return, следующий then получит undefined. А если внутри then запускается новый асинхронный шаг и его не вернули — цепочка не подождёт его.
Создаём промисы, строим цепочки, ловим ошибки, запускаем несколько промисов одновременно.
ЗАДАЧА 4.1
Первый промис
лёгкая
Создайте промис, который через 200 мс выполняется со значением "Данные загружены". Выведите значение через then.
Можно редактировать · Ctrl + Enter
// const loading = new Promise(...)// loading.then(...)
Ожидаемый результат
Данные загружены
Подсказка 1
new Promise((resolve) => { ... }) — внутри запустите setTimeout на 200 мс.
Подсказка 2
Внутри таймера вызовите resolve("Данные загружены"). Значение, переданное в resolve, придёт в колбэк then.
ЗАДАЧА 4.2
Проверка доступа
лёгкая
Функция checkAdult возвращает промис. Обработайте результат для возраста 15: выведите Ошибка: … с текстом причины, а в конце — «проверка завершена». Затем поменяйте возраст на 20 и посмотрите, что изменится.
Цепочка из трёх методов: .then(...) для успеха, .catch(...) для ошибки и .finally(...) в конце.
Подсказка 2
Причина отказа лежит в error.message: .catch((error) => console.log("Ошибка: " + error.message)).
ЗАДАЧА 4.3
Цепочка скидок
средняя
Куртка стоит 1000 сомони. Постройте цепочку из двух then: первый применяет скидку 10% и выводит После скидки: …, второй вычитает купон на 50 сомони и выводит С купоном: …. Каждый шаг должен вернуть новую цену.
Можно редактировать · Ctrl + Enter
const price = 1000;
// Promise.resolve(price)...
Ожидаемый результат
После скидки: 900
С купоном: 850
Подсказка 1
Начните с Promise.resolve(price). Каждый then получает цену из предыдущего.
Подсказка 2
Шаблон шага: .then((p) => { const next = p * 0.9; console.log("После скидки: " + next); return next; }). Второй шаг устроен так же, но вычитает 50.
ЗАДАЧА 4.4
Найди баг: потерянный return
лёгкая
Должно вывестись «Итого: 120» (цена 100 плюс доставка 20), но выводится undefined. Исправьте код.
В Promise.all передаётся массив из вызовов функций: [loadProfile(), loadOrders(), loadBanners()].
Подсказка 2
В then приходит массив результатов в том же порядке, что и в запросе. Соедините его через .join(", ").
ЗАДАЧА 4.6
Что загрузилось, а что нет
сложная
Один из трёх запросов не удался, но остальные нужно показать. В then получите массив результатов и для каждого выведите ✓ значение (если status === "fulfilled") или ✗ причина (если "rejected", причина — reason.message).
Используйте results.forEach((item) => { ... }). У каждого элемента есть поле status.
Подсказка 2
Если item.status === "fulfilled" — значение в item.value, иначе причина в item.reason.message. Порядок результатов — как в массиве запроса, а не как они завершились.
ЗАДАЧА 4.7
Предскажи порядок: цепочка с ошибкой
средняя
Ошибка в цепочке, catch, который возвращает значение, и finally. Не забудьте про синхронную строку log("f") в конце.
Синхронно выполнится только log("f"). Все остальные звенья цепочки — микрозадачи, выстроенные одна за другой.
Подсказка 2
После throw звено с меткой b пропускается — оно не вызывается. catch возвращает «d», и цепочка продолжается как успешная.
05 async / await: промисы без цепочек
Цепочки then лучше пирамиды, но всё равно читаются не как обычный код. В JavaScript есть синтаксис, который делает асинхронный код похожим на синхронный: async / await. Под капотом это те же промисы, просто записанные удобнее.
async
Слово перед функцией. Такая функция всегда возвращает промис, а внутри неё можно использовать await.
await
«Подожди результат промиса». Работает только внутри async-функции. Приостанавливает эту функцию, но не всю программу.
Те же шаги: сначала через then, затем через await · Ctrl + Enter
Код с await читается сверху вниз, как обычный синхронный: «подождали, вывели, подождали, вывели». Результат промиса можно просто положить в переменную:
Вы стоите в очереди за лепёшкой. await — это «встал в очередь и ждёшь свою порцию». Но остальные дела на улице (весь остальной код программы) не остановились: ждёте только вы, одна функция. Как только лепёшка готова, вы продолжаете с того места, где остановились.
Ошибки: try / catch / finally
Если промис отклонён, awaitбросает ошибку на том месте, где вы его ждали. Ловим её обычным try / catch, который вы уже знаете. finally выполняется в любом случае.
Можно редактировать · Ctrl + Enter
const wait = (ms) =>newPromise((resolve) =>setTimeout(resolve, ms));
asyncfunctionloadStudent(id) {
awaitwait(100);
if (id <= 0) {
thrownewError("неверный номер студента");
}
return"Студент №" + id;
}
asyncfunctionmain() {
try {
console.log(awaitloadStudent(5));
console.log(awaitloadStudent(0)); // здесь будет ошибка
console.log("эта строка не выполнится");
} catch (error) {
console.log("Ошибка:", error.message);
} finally {
console.log("Загрузка завершена");
}
}
main();
Последовательно или параллельно
Каждый await ждёт, пока завершится предыдущий шаг. Если шаги независимы — это пустая трата времени: две загрузки по 200 мс подряд займут 400 мс. Запустите оба промиса сразу и ждите их вместе через Promise.all — получится 200 мс.
Можно редактировать · Ctrl + Enter
const wait = (ms, value) =>newPromise((resolve) =>setTimeout(() =>resolve(value), ms));
asyncfunctionsequential() {
console.log("--- последовательно ---");
const a = awaitwait(200, "курсы"); // ждём 200 мсconst b = awaitwait(200, "новости"); // потом ещё 200 мс
console.log("получили:", a, b);
}
asyncfunctionparallel() {
console.log("--- параллельно ---");
const [a, b] = await Promise.all([wait(200, "курсы"), wait(200, "новости")]); // 200 мс вместе
console.log("получили:", a, b);
}
sequential().then(parallel);
Частые ошибки
1.await вне async-функции — синтаксическая ошибка. 2. Забыть await: вместо значения вы получите сам промис. 3. Забыть try / catch: ошибка «вылетит» наружу как необработанная.
Всё, что идёт послеawait, — это продолжение функции в виде микрозадачи. Код до первого await выполняется синхронно, а остаток «встаёт в очередь» микрозадач:
Можно редактировать · Ctrl + Enter
asyncfunctionmain() {
console.log("1. до await (синхронно)");
awaitnull;
console.log("3. после await (микрозадача)");
}
main();
console.log("2. после вызова main() (синхронно)");
Пишем async-функции, ловим ошибки и ускоряем независимые шаги.
ЗАДАЧА 5.1
Перепиши на async/await
лёгкая
Функция checkoutOld написана через цепочку then и не вызывается. Напишите функцию async function checkout(), которая делает то же самое через await, и вызовите её.
Функция main() выполняется синхронно до первого await.
Подсказка 2
Метка 2 стоит после await — это микрозадача, она выполнится, когда закончится весь синхронный код, включая log("3").
ЗАДАЧА 5.3
Список студентов с ошибкой
средняя
Функция fetchStudent бросает ошибку для номера 0. Напишите async-функцию main: она по очереди (в цикле for…of) загружает студентов 1, 0 и 2. Результат выводит как есть, а ошибку — как Ошибка: …. После каждого запроса, успешного или нет, выводится — запрос завершён.
Можно редактировать · Ctrl + Enter
const wait = (ms) =>newPromise((resolve) =>setTimeout(resolve, ms));
asyncfunctionfetchStudent(id) {
awaitwait(50);
if (id ===0) {
thrownewError("студент не найден");
}
return"Студент №" + id;
}
// async function main() { ... }// main();
Внутри цикла нужен свой try / catch / finally — тогда ошибка одного запроса не остановит остальные.
Подсказка 2
for (const id of [1, 0, 2]) { try { console.log(await fetchStudent(id)); } catch (error) { console.log("Ошибка: " + error.message); } finally { console.log("— запрос завершён"); } }
ЗАДАЧА 5.4
Ускорь загрузку
средняя
Две независимые загрузки сейчас идут друг за другом. Переделайте main так, чтобы обе стартовали одновременно (Promise.all). Тогда «новости» (быстрее) закончатся раньше «курсов».
старт курсы
старт новости
готов новости
готов курсы
итого: курсы + новости
Подсказка 1
Не используйте await отдельно на каждой загрузке. Запустите обе функции сразу и передайте их в Promise.all.
Подсказка 2
const [a, b] = await Promise.all([load("курсы", 200), load("новости", 100)]); — массив результатов приходит в том же порядке, что и в запросе.
ЗАДАЧА 5.5
Повторить попытку
сложная
Сервер иногда «падает». Напишите async function retry(fn, times): она вызывает fn до times раз. Если попытка неудачна — выводит попытка N: сообщение и пробует снова. Если все неудачны — выбрасывает последнюю ошибку. Функция flaky специально ломается дважды.
Цикл for (let i = 1; i <= times; i++), а внутри — try { return await fn(); } catch (error) { … }.
Подсказка 2
В catch: выведите `попытка ${i}: ${error.message}`. Если i === times, сделайте throw error — иначе цикл просто перейдёт к следующей попытке.
ЗАДАЧА 5.6
Не ждать вечно
сложная
Напишите withTimeout(promise, ms): через Promise.race она возвращает результат промиса, если он успел, или отклоняется с ошибкой таймаут, если прошло больше ms мс. Функция test уже написана.
Всё вместе: async-функции, await, then и таймер. Вспомните: функция b() выполняется синхронно до конца и возвращает уже выполненный промис, а await отдаёт управление наружу.
Синхронно: a() выполняется до await и вызывает b() целиком (метки a1, b1). Затем регистрируется then и выполняется log("s").
Подсказка 2
Микрозадачи идут в порядке добавления: сначала продолжение функции a (оно встало в очередь на await), потом then с меткой p. Таймер — последним.
06 ИИ-блок: объясняем event loop и проверяем кодом
Асинхронность — одна из тем, где ИИ объясняет красиво, но иногда ошибается в деталях: путает порядок микрозадач и таймеров или «забывает» про await. Сегодня тренируем главный навык работы с ИИ: не верить на слово, а проверять запуском.
Правила курса, которые здесь особенно важны
1. Сначала 15 минут пробуете сами: разложите код по корзинам (синхронно / микрозадачи / макрозадачи) и запишите свой прогноз порядка вывода. 2. Только потом идёте к ИИ. 3. Не вставляйте в свой проект код, который не можете объяснить строчка за строчкой. 4. Объяснение ИИ всегда проверяйте запуском: код — лучший судья.
Как просить: плохо и хорошо
Формула промпта курса: контекст → задача → ограничения → формат ответа → проверка.
объясни event loop
Я учу JavaScript, уровень — новичок, знаю функции и массивы, а про промисы только начинаю. Объясни event loop на примере этого кода: console.log("A"); setTimeout(() => console.log("B"), 0); Promise.resolve().then(() => console.log("C")); console.log("D"); Покажи порядок вывода в консоль и почему он такой: по шагам, с указанием, что лежит в стеке, в очереди микрозадач и в очереди макрозадач. Без сложных терминов и без других примеров. В конце назови 2 типичные ошибки новичков, из-за которых порядок вывода предсказывают неверно.
Скопируйте хороший промпт в чат с ИИ. Затем проверьте объяснение кодом:
Проверяем ответ ИИ запуском · Ctrl + Enter
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
// Совпал ли порядок с ответом ИИ? Если нет, значит, ИИ ошибся — или вы неточно описали задачу.
Что проверять в ответе ИИ
1) Порядок вывода совпадает с реальным запуском? 2) Правильно ли названа очередь у then и у setTimeout? 3) Нет ли в примерах чужого кода, который вы не понимаете? 4) Если ИИ предложил async/await, есть ли там try/catch?
Практика: ловим ошибку ИИ
ЗАДАЧА 6.1
ИИ ошибся? Проверь запуском
средняя
ИИ уверенно ответил, что порядок вывода будет начало, таймер, промис, конец. Не верьте — проверьте. Разложите код по корзинам, найдите ошибку в ответе ИИ и исправьте массив aiAnswer так, чтобы он совпал с реальным порядком.
Синхронные строки — «начало» и «конец» — должны идти первыми.
Подсказка 2
Промис — микрозадача, она всегда раньше таймера (макрозадачи).
07 Практика на уроке: имитация загрузки данных
Настоящих серверов у нас пока нет (до них доберёмся на следующем уроке), поэтому создадим «ненастоящий» — функции, которые отвечают с задержкой, как реальные запросы. Это стандартный приём: так проверяют интерфейс до того, как готов сервер.
Шаг 1. База данных и вспомогательные функции
Сначала наша «база» и функция wait. Запустите — пока ничего не выведется, мы просто готовим материал.
Каждая функция ждёт и возвращает данные. Если пользователя нет — отклоняет промис с ошибкой. Так себя ведёт настоящий сервер: чаще всего он отвечает за 100–500 мс, а иногда отвечает «ошибка».
В браузере мы обычно показываем пользователю три состояния: «Загрузка…», данные или ошибку. Нажмите «Загрузить» в мини-приложении ниже, затем откройте вкладку script.js и поменяйте номер пользователя на 99 — увидите состояние ошибки. К этому приёму мы вернёмся в уроке 17.
Напишите async function loadDashboard(id): она выводит «Загрузка...», затем получает пользователя и его заказы одновременно (Promise.all — они не зависят друг от друга, нужен только id) и выводит строку Привет, имя! Заказов: N, на сумму S сомони.
Доработайте loadDashboard: если пользователь не найден, выводите Ошибка: … вместо приветствия. В любом случае, успех или ошибка, в конце выводится «Загрузка завершена». Запустите функцию для пользователей 1 и 99 по очереди (дождавшись первой).
Можно редактировать · Ctrl + Enter
const wait = (ms) =>newPromise((resolve) =>setTimeout(resolve, ms));
const users = { 1: { name: "Мадина" }, 2: { name: "Фарход" } };
const orders = { 1: [120, 80, 45], 2: [] };
asyncfunctionfakeFetchUser(id) {
awaitwait(200);
if (!users[id]) thrownewError("пользователь не найден");
return users[id];
}
asyncfunctionfakeFetchOrders(id) {
awaitwait(150);
return orders[id] ?? [];
}
// async function loadDashboard(id) { ... }// запустите для 1, а когда закончится — для 99
Ожидаемый результат
Загрузка...
Привет, Мадина! Заказов: 3, на сумму 245 сомони
Загрузка завершена
Загрузка...
Ошибка: пользователь не найден
Загрузка завершена
Подсказка 1
Тело функции: «Загрузка...», затем try { ... } catch (error) { ... } finally { ... }.
Подсказка 2
Чтобы запустить по очереди, используйте цепочку: loadDashboard(1).then(() => loadDashboard(99)); — функция возвращает промис, потому что она async.
08 Домашнее задание
Шесть задач на промисы и async/await. Все решения — в одном файле async-basics.js. Перед каждым решением оставьте комментарий с номером задачи.
Таймер на промисах. Напишите функцию delay(ms), которая возвращает промис, выполняющийся через ms миллисекунд. С её помощью выведите «3», «2», «1», «Старт!» с паузой 300 мс между строками (через цепочку then).
Из колбэка в промис. Возьмите функцию checkAge(age, callback) из задачи 2.2 и напишите обёртку checkAgeAsync(age), которая возвращает промис. Вызовите её для двух возрастов, обработайте результат через then / catch.
Цепочка из трёх шагов. Три шага «заказа пиццы» с задержкой: принять заказ → приготовить → доставить. Каждый возвращает промис со строкой. Выстройте их в плоскую цепочку и выведите результат каждого шага. Добавьте один catch в конце и проверьте, что он срабатывает, если заставить один шаг отклониться.
Три загрузки одновременно. Три «запроса» с разной задержкой (курс сомони, погода в Душанбе, расписание группы). Запустите через Promise.all, выведите результаты и общее время (Date.now() до и после — оно должно быть около самой долгой задержки, а не суммы).
async / await с ошибкой. Напишите async function loadProfile(id), которая «грузит» профиль 300 мс и для номера больше 100 бросает ошибку. Вызовите её для трёх номеров, ошибки ловите через try / catch / finally.
Лист прогнозов. Придумайте три собственных фрагмента кода со смесью console.log, setTimeout и Promise.then. Над каждым запишите комментарием свой прогноз порядка вывода, затем запустите и сверьте. Если не угадали — одной строкой объясните, в чём была ошибка.
Что сдавать
Файл async-basics.js со всеми шестью решениями и комментариями.
Ссылку на репозиторий в GitHub (или сам файл — как договоритесь с преподавателем).
Как оцениваем (0–10)
Что
Баллы
Задачи 1–2 работают и выводят ожидаемый результат
2
Задача 3: плоская цепочка, catch реально срабатывает
2
Задача 4: запросы идут одновременно, время около самой долгой задержки
2
Задача 5: try / catch / finally на месте и ловит ошибки
2
Задача 6: честные прогнозы, ошибки разобраны
2
Перед сдачей
Для каждой функции спросите себя: «Могу ли я объяснить, в какой очереди выполнится каждая строка?» Если вы использовали ИИ — каждую строку вы должны уметь объяснить на защите.
✓ Шпаргалка урока
Синхронный код идёт по строкам. Асинхронный запускает работу и не ждёт: результат приходит позже.
setTimeout(fn, мс) — выполнить один раз позже; setInterval — повторять; остановка — clearTimeout / clearInterval. Передаём функцию, а не её вызов.
Callback — функция, которую вызовут «потом». Колбэк «error-first»: (error, result). Много вложенных колбэков — «ад колбэков».
Promise: pending → fulfilled / rejected, меняется один раз. Создание: new Promise((resolve, reject) => …).
then / catch / finally: then возвращает новый промис, значение уходит по цепочке (нужен return); ошибка пропускает then до ближайшего catch; finally — всегда.
Promise.all — все вместе (порядок как в запросе, одна ошибка — всё отклонено); allSettled — отчёт по каждому; race — самый быстрый.
async функция всегда возвращает промис. await ждёт результат только внутри async; ошибки ловим try / catch / finally. Независимые шаги — через Promise.all.
Event loop: сначала весь синхронный код → все микрозадачи (then, после await, queueMicrotask) → одна макрозадача (setTimeout) → снова микрозадачи.
С ИИ: сначала сами, потом промпт по формуле. Объяснение ИИ про порядок вывода — всегда проверяем запуском.