MakeFirstStep· Frontend + ИИ

Урок 13 · Async и API

Асинхронный JavaScript

Когда вы открываете сайт, он не замирает, пока загружаются картинки, курс валют или список товаров. Страница живёт, а данные «приезжают» позже. Это асинхронность: JavaScript умеет не ждать у плиты, а заниматься другими делами, пока готовится ответ. Сегодня разберём, как это устроено, и научимся писать такой код без путаницы.

  • Объяснять разницу между синхронным и асинхронным кодом
  • Откладывать действия через setTimeout и повторять через setInterval
  • Читать код с колбэками и понимать, чем плох «ад колбэков»
  • Создавать промисы и строить цепочки then / catch / finally
  • Писать async / await с try / catch
  • Предсказывать порядок вывода: стек, микрозадачи, макрозадачи (event loop)
Как работать с этой страницей

Весь код на тёмном фоне с кнопкой ▶ Запустить — живой. Его можно менять и сразу видеть результат в консоли. В этом уроке код часто что-то выводит с задержкой — подождите секунду, пока консоль доберёт строки. Много задач вида «предскажи порядок вывода»: сначала думайте и записывайте прогноз, потом запускайте. Подсказки спрятаны под замочком 🔒 — открывайте, только если застряли.

01 Синхронно и асинхронно. setTimeout

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

Синхронный код: строго сверху вниз · Ctrl + Enter
console.log("1. Открыли приложение");
console.log("2. Посчитали сумму");
console.log("3. Показали результат");

Но в реальных программах есть дела, которые занимают время: запрос на сервер, загрузка картинки, таймер, ответ пользователя. Если ждать каждое из них, пока оно закончится, страница «зависнет». Поэтому такие дела запускают и не ждут: когда результат будет готов, 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");
  1. A — обычная строка, выводится сразу.
  2. setTimeout — функция «отложена» в сторону. Дальше JavaScript идёт, не оглядываясь.
  3. C — выводится сразу.
  4. Весь код закончился, и только теперь 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
function sayHi() {
  console.log("Салом!");
}

setTimeout(sayHi(), 1000);   // ✖ «Салом!» выведется мгновенно, а не через секунду
console.log("Конец кода");

Читать подробнее: learn.javascript.ru — setTimeout и setInterval · MDN — setTimeout

Практика: закрепляем

Большинство задач этого урока — про порядок, в котором строки попадут в консоль. В задачах «предскажи порядок» код сам сверяет ваш прогноз с реальностью: впишите ответ в массив 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"]). Затем запустите — проверка в конце скажет, угадали ли вы.

Можно редактировать · Ctrl + Enter
const actual = [];
const log = (label) => actual.push(label);

log("1");
setTimeout(() => log("2"), 100);
setTimeout(() => log("3"), 50);
log("4");

const prediction = [];   // ваш прогноз: порядок меток

// --- проверка, не меняйте ---
setTimeout(() => console.log(actual.join(" ") === prediction.join(" ") ? "Верно!" : "Не угадали, подумайте ещё"), 400);
Ожидаемый результат
Верно!
Подсказка 1

Сначала выполняется весь синхронный код — это метки 1 и 4. Таймеры подождут.

Подсказка 2

Из таймеров раньше сработает тот, у которого меньше задержка: 50 мс раньше, чем 100 мс — независимо от порядка записи в коде.

ЗАДАЧА 1.3

Старт гонки

средняя

Сделайте обратный отсчёт: каждые 100 мс выводите число, начиная с 3. Когда число дойдёт до 0, выведите «Старт!» (вместо нуля) и остановите таймер.

Можно редактировать · Ctrl + Enter
let n = 3;

// запустите setInterval на 100 мс
Ожидаемый результат
3
2
1
Старт!
Подсказка 1

Сохраните номер таймера: const id = setInterval(…) — он понадобится для clearInterval(id).

Подсказка 2

Внутри колбэка: если n === 0 — выведите «Старт!», остановите таймер и сделайте return. Иначе выведите n и уменьшите его на 1.

ЗАДАЧА 1.4

Найди баг: таймер не ждёт

средняя

Сообщение «Таймер сработал» должно появиться после «Конец», через 200 мс. Сейчас оно появляется сразу. Найдите ошибку и исправьте.

Можно редактировать · Ctrl + Enter
console.log("Начало");

setTimeout(console.log("Таймер сработал"), 200);

console.log("Конец");
Ожидаемый результат
Начало
Конец
Таймер сработал
Подсказка 1

Скобки у console.log(...) вызывают функцию немедленно. В setTimeout нужно передать функцию, а не результат её вызова.

Подсказка 2

Оберните вызов в стрелочную функцию: () => console.log("Таймер сработал").

02 Callback: «перезвони, когда будет готово»

Мы уже передавали функции в map и filter. Функция, которую передают в другую функцию «на потом», называется колбэком. Раньше колбэки вызывались сразу, внутри цикла. А в асинхронном коде колбэк вызывается, когда работа закончится.

Функция сообщает о результате через колбэк · Ctrl + Enter
function loadPrice(dish, callback) {
  console.log("Спрашиваем цену:", dish);
  setTimeout(() => {
    const price = dish === "плов" ? 35 : 10;
    callback(price);            // «перезваниваем» с результатом
  }, 300);
}

loadPrice("плов", (price) => {
  console.log("Цена плова:", price, "сомони");
});

console.log("Пока ждём ответ, можно заняться другими делами");
Аналогия из жизни

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

Колбэк с ошибкой: договорённость «error-first»

Асинхронная работа может не получиться: нет сети, нет такого товара, неверный пароль. Поэтому по традиции первым аргументом колбэк получает ошибку (или null, если всё хорошо), а вторым — результат.

Колбэк (error, result) · Ctrl + Enter
function findStudent(id, callback) {
  setTimeout(() => {
    if (id !== 1) {
      callback(new Error("студент не найден"));   // ошибка — первый аргумент
      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)

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

Пирамида судьбы · Ctrl + Enter
function step(name, callback) {
  setTimeout(() => {
    console.log("Готово:", name);
    callback();
  }, 100);
}

step("нашли пользователя", () => {
  step("загрузили заказы", () => {
    step("узнали адрес", () => {
      step("посчитали доставку", () => {
        console.log("Всё готово!");
      });
    });
  });
});

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

Колбэки

Вложенность растёт вправо. Ошибки нужно проверять на каждом уровне вручную. Код сложно менять.

Промисы и async/await

Шаги идут друг за другом ровной колонкой. Ошибки ловит одно место — catch или try/catch.

Частая ошибка

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

Ошибка: результата ещё нет · Ctrl + Enter
let price;

setTimeout(() => {
  price = 35;
}, 200);

console.log("Цена плова:", price);   // ✖ undefined — таймер ещё не сработал

Всё, что зависит от результата, нужно делать внутри колбэка (или, как мы увидим дальше, после await).

Читать подробнее: learn.javascript.ru — Введение: колбэки · MDN — Callback function

Практика: закрепляем

Пишем свои асинхронные функции с колбэками и обрабатываем ошибки.

ЗАДАЧА 2.1

Меню с задержкой

лёгкая

Напишите функцию loadMenu(callback): через 200 мс она вызывает callback и передаёт ему массив ["самбуса", "курутоб", "чой"]. Вызов функции уже написан.

Можно редактировать · Ctrl + Enter
// function loadMenu(callback) { ... }

loadMenu((menu) => {
  console.log("Меню загружено, блюд:", menu.length);
  console.log(menu.join(", "));
});
Ожидаемый результат
Меню загружено, блюд: 3
самбуса, курутоб, чой
Подсказка 1

Внутри функции нужен setTimeout на 200 мс. Колбэк вызывается внутри таймера.

Подсказка 2

setTimeout(() => callback(["самбуса", "курутоб", "чой"]), 200);

ЗАДАЧА 2.2

Проверка возраста

средняя

Функция checkAge готова и сообщает результат по правилу error-first. Вызовите её дважды — для возраста 20 и для 15 — и в колбэке выведите либо сообщение об успехе, либо Ошибка: … с текстом ошибки.

Можно редактировать · Ctrl + Enter
function checkAge(age, callback) {
  setTimeout(() => {
    if (age < 18) {
      callback(new Error("слишком мал"));
    } else {
      callback(null, "Добро пожаловать!");
    }
  }, 100);
}

// вызовите checkAge(20, …) и checkAge(15, …)
Ожидаемый результат
Добро пожаловать!
Ошибка: слишком мал
Подсказка 1

Колбэк принимает два параметра: (error, result). Сначала проверяем error.

Подсказка 2

(error, result) => { if (error) { console.log("Ошибка: " + error.message); return; } console.log(result); } — этот колбэк можно использовать в обоих вызовах.

ЗАДАЧА 2.3

Заказ плова по шагам

лёгкая

Три шага заказа уже написаны, каждый занимает 100 мс. Запустите их друг за другом: следующий шаг вызывается в колбэке предыдущего. Каждый шаг передаёт в колбэк строку — выведите её.

Можно редактировать · Ctrl + Enter
function takeOrder(callback) { setTimeout(() => callback("заказ принят"), 100); }
function cook(callback)      { setTimeout(() => callback("плов приготовлен"), 100); }
function serve(callback)     { setTimeout(() => callback("плов на столе"), 100); }

// выстройте цепочку: takeOrder → cook → serve
Ожидаемый результат
заказ принят
плов приготовлен
плов на столе
Подсказка 1

Начните с 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. После каждой макрозадачи выполняются все, пока очередь не опустеет.

Разбор по шагам

Классика собеседований · Ctrl + Enter
console.log("1");

setTimeout(() => console.log("2"), 0);

Promise.resolve().then(() => console.log("3"));

console.log("4");
  1. Синхронный код. В стек попадает console.log("1") — выводится 1.
  2. setTimeout. Таймер передан браузеру. Через 0 мс его колбэк ляжет в очередь макрозадач. Выполнен он будет не сейчас.
  3. Promise.resolve().then(…). Промис уже выполнен, поэтому колбэк then сразу ложится в очередь микрозадач.
  4. Синхронный код. console.log("4") — выводится 4. Стек опустел.
  5. Микрозадачи. Event loop видит, что очередь микрозадач не пуста, и выполняет колбэк — выводится 3.
  6. Макрозадача. Теперь очередь микрозадач пуста, и можно взять одну макрозадачу — колбэк таймера: выводится 2.

Итог: 1, 4, 3, 2. Запомните правило: синхронный код → микрозадачи (промисы) → макрозадачи (таймеры).

Микрозадачи вклиниваются после каждой макрозадачи

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

Можно редактировать · Ctrl + Enter
setTimeout(() => {
  console.log("таймер 1");
  Promise.resolve().then(() => console.log("промис внутри таймера 1"));
}, 0);

setTimeout(() => console.log("таймер 2"), 0);

Порядок: таймер 1, промис внутри таймера 1, таймер 2. Микрозадача, рождённая таймером 1, успела раньше, чем «обычная очередь» дошла до таймера 2.

Частая ошибка

Думать, что setTimeout(fn, 0) сработает «сразу». На самом деле он сработает только когда закончится весь текущий код и все микрозадачи. А если текущий код занят тяжёлым циклом, таймер будет ждать, пока цикл не завершится: setTimeout гарантирует «не раньше, чем через N мс», а не «ровно через N мс».

Совет: как предсказывать порядок

Идите по коду сверху вниз и раскладывайте строки по трём «корзинам»: 1. выполняется сразу, 2. в микрозадачи (then, await), 3. в макрозадачи (setTimeout; у кого задержка меньше — тот раньше). Потом читайте: корзина 1 → корзина 2 → корзина 3. Если внутри колбэка появилась новая корзина-2 или корзина-3, повторите то же самое.

Читать подробнее: learn.javascript.ru — Событийный цикл · learn.javascript.ru — Микрозадачи · MDN — Event loop

Практика: предскажи порядок вывода

Это главная тренировка урока. Для каждой задачи сначала разложите строки по корзинам на бумаге, впишите ответ в prediction и только потом запускайте.

ЗАДАЧА 3.1

Три буквы

лёгкая

Впишите порядок меток.

Можно редактировать · Ctrl + Enter
const actual = [];
const log = (label) => actual.push(label);

log("A");
setTimeout(() => log("B"), 0);
log("C");

const prediction = [];   // например: ["A", "B", "C"]

// --- проверка, не меняйте ---
setTimeout(() => console.log(actual.join(" ") === prediction.join(" ") ? "Верно!" : "Не угадали, подумайте ещё"), 400);
Ожидаемый результат
Верно!
Подсказка 1

Таймер, даже нулевой, уходит в очередь макрозадач — после всего синхронного кода.

Подсказка 2

Синхронно выполняются log("A") и log("C"). Потом — таймер с меткой B.

ЗАДАЧА 3.2

Промис против таймера

средняя

Кто быстрее: промис или таймер с нулевой задержкой?

Можно редактировать · Ctrl + Enter
const actual = [];
const log = (label) => actual.push(label);

log("1");
setTimeout(() => log("2"), 0);
Promise.resolve().then(() => log("3"));
log("4");

const prediction = [];

// --- проверка, не меняйте ---
setTimeout(() => console.log(actual.join(" ") === prediction.join(" ") ? "Верно!" : "Не угадали, подумайте ещё"), 400);
Ожидаемый результат
Верно!
Подсказка 1

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

Подсказка 2

Порядок корзин: сразу (1, 4) → микрозадачи (3) → макрозадачи (2).

ЗАДАЧА 3.3

Две микрозадачи

лёгкая

Здесь два способа создать микрозадачу: queueMicrotask и then. Они стоят в одной очереди — по порядку добавления.

Можно редактировать · Ctrl + Enter
const actual = [];
const log = (label) => actual.push(label);

setTimeout(() => log("таймер"), 0);
queueMicrotask(() => log("микро-1"));
Promise.resolve().then(() => log("микро-2"));
log("синхронно");

const prediction = [];   // четыре метки: синхронная, две микро и таймер

// --- проверка, не меняйте ---
setTimeout(() => console.log(actual.join(" ") === prediction.join(" ") ? "Верно!" : "Не угадали, подумайте ещё"), 400);
Ожидаемый результат
Верно!
Подсказка 1

Таймер всегда в самом конце, а синхронная строка — в самом начале.

Подсказка 2

Между ними микрозадачи в том порядке, в каком они были добавлены в очередь: сначала queueMicrotask, потом then.

ЗАДАЧА 3.4

Вложенные очереди

сложная

Задача посложнее: колбэки сами создают новые таймеры и промисы. Помните правило: после каждой макрозадачи выполняются все накопившиеся микрозадачи.

Можно редактировать · Ctrl + Enter
const actual = [];
const log = (label) => actual.push(label);

setTimeout(() => {
  log("A");
  Promise.resolve().then(() => log("B"));
}, 0);

setTimeout(() => log("C"), 0);

Promise.resolve().then(() => {
  log("D");
  setTimeout(() => log("E"), 0);
});

log("F");

const prediction = [];

// --- проверка, не меняйте ---
setTimeout(() => console.log(actual.join(" ") === prediction.join(" ") ? "Верно!" : "Не угадали, подумайте ещё"), 400);
Ожидаемый результат
Верно!
Подсказка 1

Сначала F (синхронно), затем единственная микрозадача D. Она кладёт в очередь макрозадач ещё один таймер — E, в конец очереди, после A и C.

Подсказка 2

Очередь макрозадач после первого круга: A, C, E. Но после выполнения A сразу же срабатывает его микрозадача B — раньше, чем C.

ЗАДАЧА 3.5

Задержки и цепочка

сложная

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

Можно редактировать · Ctrl + Enter
const actual = [];
const log = (label) => actual.push(label);

setTimeout(() => log("T200"), 200);
setTimeout(() => log("T0"), 0);
Promise.resolve().then(() => log("P1")).then(() => log("P2"));
setTimeout(() => log("T100"), 100);
log("S");

const prediction = [];

// --- проверка, не меняйте ---
setTimeout(() => console.log(actual.join(" ") === prediction.join(" ") ? "Верно!" : "Не угадали, подумайте ещё"), 400);
Ожидаемый результат
Верно!
Подсказка 1

Синхронно — только 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
const orderPlov = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Плов готов!");     // успех: передаём значение
  }, 300);
});

orderPlov.then((result) => {
  console.log("Получили:", result);
});

console.log("Промис создан, ждём…");
Промис, который отклоняется · Ctrl + Enter
function buyTicket(balance) {
  return new Promise((resolve, reject) => {
    if (balance >= 3) {
      resolve("Билет куплен");
    } else {
      reject(new Error("Недостаточно сомони"));
    }
  });
}

buyTicket(10)
  .then((text) => console.log(text))
  .catch((error) => console.log("Ошибка:", error.message));

buyTicket(1)
  .then((text) => console.log(text))
  .catch((error) => console.log("Ошибка:", error.message));

then, catch, finally

  • .then(onSuccess) — выполнится, когда промис выполнен;
  • .catch(onError) — выполнится, когда промис отклонён (или где-то выше по цепочке случилась ошибка);
  • .finally(fn) — выполнится в любом случае: «прибраться» после работы (убрать индикатор загрузки).
Можно редактировать · Ctrl + Enter
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

console.log("Показываем «Загрузка...»");

wait(300)
  .then(() => console.log("Данные пришли"))
  .catch(() => console.log("Ошибка"))
  .finally(() => console.log("Прячем «Загрузка...»"));

Функция 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 возвращает промис, цепочка подождёт его завершения и только потом пойдёт дальше. Так выстраиваются последовательные шаги, и пирамида колбэков из прошлого раздела превращается в ровную колонку:

Те же четыре шага, но без пирамиды · Ctrl + Enter
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function step(name) {
  return wait(100).then(() => console.log("Готово:", name));
}

step("нашли пользователя")
  .then(() => step("загрузили заказы"))
  .then(() => step("узнали адрес"))
  .then(() => step("посчитали доставку"))
  .then(() => console.log("Всё готово!"));

Ошибки в цепочке

Если в любом звене случилась ошибка (reject или throw), цепочка пропускает все следующие then и «проваливается» в ближайший catch. Один catch в конце ловит ошибки всех шагов. А если catch вернул значение — цепочка снова считается успешной и идёт дальше.

Можно редактировать · Ctrl + Enter
Promise.resolve("шаг 1")
  .then((v) => {
    console.log(v, "выполнен");
    throw new Error("шаг 2 сломался");
  })
  .then(() => console.log("этот then пропущен"))
  .catch((error) => {
    console.log("Поймали:", error.message);
    return "запасной вариант";
  })
  .then((v) => console.log("Цепочка продолжилась:", v));
Частая ошибка: забыли return в then

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

Цена потерялась · Ctrl + Enter
Promise.resolve(100)
  .then((price) => {
    price * 2;                      // ✖ посчитали, но не вернули
  })
  .then((result) => {
    console.log("Результат:", result);   // → undefined
  });

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

Бывает, что нужно загрузить несколько независимых вещей. Если ждать их по очереди — потеряем время. Лучше запустить все сразу.

  • Promise.all([…]) — ждёт все. Возвращает массив результатов в том же порядке, что и в запросе. Если хотя бы один промис отклонён — отклоняется целиком;
  • Promise.allSettled([…]) — ждёт все и никогда не отклоняется: для каждого сообщает status ("fulfilled" или "rejected") и value / reason;
  • Promise.race([…]) — результат (или ошибка) самого быстрого.
Можно редактировать · Ctrl + Enter
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));

Promise.all([
  wait(300, "курсы валют"),
  wait(100, "погода"),
  wait(200, "новости"),
]).then((results) => {
  console.log("Всё загружено:", results.join(", "));   // порядок как в запросе
});

Promise.race([wait(300, "медленный"), wait(100, "быстрый")])
  .then((winner) => console.log("Первым пришёл:", winner));

Читать подробнее: learn.javascript.ru — Промисы · learn.javascript.ru — Цепочка промисов · MDN — Promise

Практика: закрепляем

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

ЗАДАЧА 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 и посмотрите, что изменится.

Можно редактировать · Ctrl + Enter
function checkAdult(age) {
  return new Promise((resolve, reject) => {
    if (age >= 18) {
      resolve("доступ разрешён");
    } else {
      reject(new Error("доступ закрыт"));
    }
  });
}

// checkAdult(15) ...
Ожидаемый результат
Ошибка: доступ закрыт
проверка завершена
Подсказка 1

Цепочка из трёх методов: .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. Исправьте код.

Можно редактировать · Ctrl + Enter
Promise.resolve(100)
  .then((price) => {
    const total = price + 20;
  })
  .then((total) => {
    console.log("Итого: " + total);
  });
Ожидаемый результат
Итого: 120
Подсказка 1

Что получает второй then? Только то, что вернул первый.

Подсказка 2

Добавьте в первый then строку return total;.

ЗАДАЧА 4.5

Загрузка главной страницы

средняя

Три независимые загрузки занимают 300, 100 и 200 мс. Запустите их одновременно через Promise.all и выведите одну строку, как в примере.

Можно редактировать · Ctrl + Enter
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));

const loadProfile = () => wait(300, "профиль");
const loadOrders  = () => wait(100, "заказы");
const loadBanners = () => wait(200, "баннеры");

// Promise.all([...]).then(...)
Ожидаемый результат
Загружено: профиль, заказы, баннеры
Подсказка 1

В Promise.all передаётся массив из вызовов функций: [loadProfile(), loadOrders(), loadBanners()].

Подсказка 2

В then приходит массив результатов в том же порядке, что и в запросе. Соедините его через .join(", ").

ЗАДАЧА 4.6

Что загрузилось, а что нет

сложная

Один из трёх запросов не удался, но остальные нужно показать. В then получите массив результатов и для каждого выведите ✓ значение (если status === "fulfilled") или ✗ причина (если "rejected", причина — reason.message).

Можно редактировать · Ctrl + Enter
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
const fail = (ms, message) => new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms));

Promise.allSettled([
  wait(100, "курс"),
  fail(150, "нет сети"),
  wait(50, "расписание"),
]).then((results) => {
  // выведите по строке для каждого результата
});
Ожидаемый результат
✓ курс
✗ нет сети
✓ расписание
Подсказка 1

Используйте results.forEach((item) => { ... }). У каждого элемента есть поле status.

Подсказка 2

Если item.status === "fulfilled" — значение в item.value, иначе причина в item.reason.message. Порядок результатов — как в массиве запроса, а не как они завершились.

ЗАДАЧА 4.7

Предскажи порядок: цепочка с ошибкой

средняя

Ошибка в цепочке, catch, который возвращает значение, и finally. Не забудьте про синхронную строку log("f") в конце.

Можно редактировать · Ctrl + Enter
const actual = [];
const log = (label) => actual.push(label);

Promise.resolve("a")
  .then((v) => {
    log(v);
    throw new Error("сбой");
  })
  .then(() => log("b"))
  .catch(() => {
    log("c");
    return "d";
  })
  .then((v) => log(v))
  .finally(() => log("e"));

log("f");

const prediction = [];

// --- проверка, не меняйте ---
setTimeout(() => console.log(actual.join(" ") === prediction.join(" ") ? "Верно!" : "Не угадали, подумайте ещё"), 400);
Ожидаемый результат
Верно!
Подсказка 1

Синхронно выполнится только log("f"). Все остальные звенья цепочки — микрозадачи, выстроенные одна за другой.

Подсказка 2

После throw звено с меткой b пропускается — оно не вызывается. catch возвращает «d», и цепочка продолжается как успешная.

05 async / await: промисы без цепочек

Цепочки then лучше пирамиды, но всё равно читаются не как обычный код. В JavaScript есть синтаксис, который делает асинхронный код похожим на синхронный: async / await. Под капотом это те же промисы, просто записанные удобнее.

async
Слово перед функцией. Такая функция всегда возвращает промис, а внутри неё можно использовать await.
await
«Подожди результат промиса». Работает только внутри async-функции. Приостанавливает эту функцию, но не всю программу.
Те же шаги: сначала через then, затем через await · Ctrl + Enter
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// было: цепочка then
function checkoutThen() {
  return wait(100)
    .then(() => console.log("then: корзина открыта"))
    .then(() => wait(100))
    .then(() => console.log("then: оплата прошла"));
}

// стало: async / await
async function checkout() {
  await wait(100);
  console.log("await: корзина открыта");
  await wait(100);
  console.log("await: оплата прошла");
}

checkoutThen().then(() => checkout());

Код с await читается сверху вниз, как обычный синхронный: «подождали, вывели, подождали, вывели». Результат промиса можно просто положить в переменную:

Можно редактировать · Ctrl + Enter
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));

async function showPrice() {
  console.log("Спрашиваем цену...");
  const price = await wait(200, 35);      // await «достаёт» значение из промиса
  console.log("Цена плова:", price, "сомони");
  return price * 2;
}

showPrice().then((double) => console.log("Две порции:", double));
console.log("showPrice() запущена и вернула промис");
Аналогия из жизни

Вы стоите в очереди за лепёшкой. await — это «встал в очередь и ждёшь свою порцию». Но остальные дела на улице (весь остальной код программы) не остановились: ждёте только вы, одна функция. Как только лепёшка готова, вы продолжаете с того места, где остановились.

Ошибки: try / catch / finally

Если промис отклонён, await бросает ошибку на том месте, где вы его ждали. Ловим её обычным try / catch, который вы уже знаете. finally выполняется в любом случае.

Можно редактировать · Ctrl + Enter
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function loadStudent(id) {
  await wait(100);
  if (id <= 0) {
    throw new Error("неверный номер студента");
  }
  return "Студент №" + id;
}

async function main() {
  try {
    console.log(await loadStudent(5));
    console.log(await loadStudent(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) => new Promise((resolve) => setTimeout(() => resolve(value), ms));

async function sequential() {
  console.log("--- последовательно ---");
  const a = await wait(200, "курсы");        // ждём 200 мс
  const b = await wait(200, "новости");      // потом ещё 200 мс
  console.log("получили:", a, b);
}

async function parallel() {
  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 · Ctrl + Enter
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));

async function main() {
  const price = wait(100, 35);         // ✖ без await
  console.log("Цена:", price);          // → Цена: Promise {…}, а не 35
}

main();

await и event loop

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

Можно редактировать · Ctrl + Enter
async function main() {
  console.log("1. до await (синхронно)");
  await null;
  console.log("3. после await (микрозадача)");
}

main();
console.log("2. после вызова main() (синхронно)");

Читать подробнее: learn.javascript.ru — Async/await · MDN — Использование промисов

Практика: закрепляем

Пишем async-функции, ловим ошибки и ускоряем независимые шаги.

ЗАДАЧА 5.1

Перепиши на async/await

лёгкая

Функция checkoutOld написана через цепочку then и не вызывается. Напишите функцию async function checkout(), которая делает то же самое через await, и вызовите её.

Можно редактировать · Ctrl + Enter
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function checkoutOld() {
  return wait(100)
    .then(() => { console.log("Шаг 1: открыли корзину"); return wait(100); })
    .then(() => { console.log("Шаг 2: выбрали доставку"); return wait(100); })
    .then(() => console.log("Шаг 3: оплатили"));
}

// async function checkout() { ... }
Ожидаемый результат
Шаг 1: открыли корзину
Шаг 2: выбрали доставку
Шаг 3: оплатили
Подсказка 1

Каждый .then(() => ...) превращается в строку: сначала await wait(100), затем console.log(...).

Подсказка 2

Тело: await wait(100); console.log("Шаг 1: ..."); await wait(100); console.log("Шаг 2: ..."); await wait(100); console.log("Шаг 3: ..."); — и не забудьте вызвать checkout().

ЗАДАЧА 5.2

Предскажи порядок: await

лёгкая

Код после await — микрозадача. Что произойдёт с метками 1, 2, 3, 0?

Можно редактировать · Ctrl + Enter
const actual = [];
const log = (label) => actual.push(label);

async function main() {
  log("1");
  await null;
  log("2");
}

log("0");
main();
log("3");

const prediction = [];

// --- проверка, не меняйте ---
setTimeout(() => console.log(actual.join(" ") === prediction.join(" ") ? "Верно!" : "Не угадали, подумайте ещё"), 400);
Ожидаемый результат
Верно!
Подсказка 1

Функция main() выполняется синхронно до первого await.

Подсказка 2

Метка 2 стоит после await — это микрозадача, она выполнится, когда закончится весь синхронный код, включая log("3").

ЗАДАЧА 5.3

Список студентов с ошибкой

средняя

Функция fetchStudent бросает ошибку для номера 0. Напишите async-функцию main: она по очереди (в цикле for…of) загружает студентов 1, 0 и 2. Результат выводит как есть, а ошибку — как Ошибка: …. После каждого запроса, успешного или нет, выводится — запрос завершён.

Можно редактировать · Ctrl + Enter
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function fetchStudent(id) {
  await wait(50);
  if (id === 0) {
    throw new Error("студент не найден");
  }
  return "Студент №" + id;
}

// async function main() { ... }
// main();
Ожидаемый результат
Студент №1
— запрос завершён
Ошибка: студент не найден
— запрос завершён
Студент №2
— запрос завершён
Подсказка 1

Внутри цикла нужен свой 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). Тогда «новости» (быстрее) закончатся раньше «курсов».

Можно редактировать · Ctrl + Enter
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function load(name, ms) {
  console.log("старт " + name);
  await wait(ms);
  console.log("готов " + name);
  return name;
}

async function main() {
  const a = await load("курсы", 200);
  const b = await load("новости", 100);
  console.log("итого: " + a + " + " + b);
}

main();
Ожидаемый результат
старт курсы
старт новости
готов новости
готов курсы
итого: курсы + новости
Подсказка 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 специально ломается дважды.

Можно редактировать · Ctrl + Enter
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

let calls = 0;
async function flaky() {
  calls++;
  await wait(50);
  if (calls < 3) {
    throw new Error("сбой " + calls);
  }
  return "данные";
}

// async function retry(fn, times) { ... }

retry(flaky, 5).then((result) => console.log("успех: " + result));
Ожидаемый результат
попытка 1: сбой 1
попытка 2: сбой 2
успех: данные
Подсказка 1

Цикл 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 уже написана.

Можно редактировать · Ctrl + Enter
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));

// function withTimeout(promise, ms) { ... }

async function test(label, delay) {
  try {
    console.log(await withTimeout(wait(delay, "ответ " + label), 150));
  } catch (error) {
    console.log("Ошибка: " + error.message);
  }
}

test("быстрый", 50).then(() => test("медленный", 400));
Ожидаемый результат
ответ быстрый
Ошибка: таймаут
Подсказка 1

Нужна «гонка» между вашим промисом и вторым промисом, который просто отклоняется через ms миллисекунд.

Подсказка 2

return Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(new Error("таймаут")), ms))]);

ЗАДАЧА 5.7

Предскажи порядок: финал

сложная

Всё вместе: async-функции, await, then и таймер. Вспомните: функция b() выполняется синхронно до конца и возвращает уже выполненный промис, а await отдаёт управление наружу.

Можно редактировать · Ctrl + Enter
const actual = [];
const log = (label) => actual.push(label);

async function a() {
  log("a1");
  await b();
  log("a2");
}

async function b() {
  log("b1");
}

setTimeout(() => log("t"), 0);
a();
Promise.resolve().then(() => log("p"));
log("s");

const prediction = [];

// --- проверка, не меняйте ---
setTimeout(() => console.log(actual.join(" ") === prediction.join(" ") ? "Верно!" : "Не угадали, подумайте ещё"), 400);
Ожидаемый результат
Верно!
Подсказка 1

Синхронно: 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 так, чтобы он совпал с реальным порядком.

Можно редактировать · Ctrl + Enter
const actual = [];
const log = (label) => actual.push(label);

log("начало");
setTimeout(() => log("таймер"), 0);
Promise.resolve().then(() => log("промис"));
log("конец");

// ответ ИИ — в нём ошибка, исправьте
const aiAnswer = ["начало", "таймер", "промис", "конец"];

// --- проверка, не меняйте ---
setTimeout(() => console.log(actual.join(" ") === aiAnswer.join(" ") ? "Верно!" : "ИИ ошибся, исправьте ответ"), 400);
Ожидаемый результат
Верно!
Подсказка 1

Синхронные строки — «начало» и «конец» — должны идти первыми.

Подсказка 2

Промис — микрозадача, она всегда раньше таймера (макрозадачи).

07 Практика на уроке: имитация загрузки данных

Настоящих серверов у нас пока нет (до них доберёмся на следующем уроке), поэтому создадим «ненастоящий» — функции, которые отвечают с задержкой, как реальные запросы. Это стандартный приём: так проверяют интерфейс до того, как готов сервер.

Шаг 1. База данных и вспомогательные функции

Сначала наша «база» и функция wait. Запустите — пока ничего не выведется, мы просто готовим материал.

Шаг 1. «Сервер» · Ctrl + Enter
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const users = {
  1: { name: "Мадина" },
  2: { name: "Фарход" },
};

const orders = {
  1: [120, 80, 45],   // суммы заказов в сомони
  2: [],
};

console.log("Готово. Пользователей:", Object.keys(users).length);

Шаг 2. Функции «запросов»

Каждая функция ждёт и возвращает данные. Если пользователя нет — отклоняет промис с ошибкой. Так себя ведёт настоящий сервер: чаще всего он отвечает за 100–500 мс, а иногда отвечает «ошибка».

Шаг 2. Запросы с задержкой · Ctrl + Enter
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const users = { 1: { name: "Мадина" }, 2: { name: "Фарход" } };
const orders = { 1: [120, 80, 45], 2: [] };

async function fakeFetchUser(id) {
  await wait(200);
  const user = users[id];
  if (!user) {
    throw new Error("пользователь не найден");
  }
  return user;
}

async function fakeFetchOrders(id) {
  await wait(150);
  return orders[id] ?? [];
}

fakeFetchUser(1).then((user) => console.log("Получили:", user.name));
fakeFetchUser(99).catch((error) => console.log("Ошибка:", error.message));

Шаг 3. Цепочка: пользователь → его заказы

Заказы зависят от пользователя, поэтому ждём по очереди. Через async/await это читается как рецепт:

Шаг 3. Зависимые запросы · Ctrl + Enter
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const users = { 1: { name: "Мадина" }, 2: { name: "Фарход" } };
const orders = { 1: [120, 80, 45], 2: [] };

async function fakeFetchUser(id) {
  await wait(200);
  if (!users[id]) throw new Error("пользователь не найден");
  return users[id];
}
async function fakeFetchOrders(id) {
  await wait(150);
  return orders[id] ?? [];
}

async function showProfile(id) {
  const user = await fakeFetchUser(id);
  const list = await fakeFetchOrders(id);
  const sum = list.reduce((total, price) => total + price, 0);
  console.log(`${user.name}: заказов ${list.length}, на ${sum} сомони`);
}

showProfile(1);

Шаг 4. Покажем это на странице

В браузере мы обычно показываем пользователю три состояния: «Загрузка…», данные или ошибку. Нажмите «Загрузить» в мини-приложении ниже, затем откройте вкладку script.js и поменяйте номер пользователя на 99 — увидите состояние ошибки. К этому приёму мы вернёмся в уроке 17.

Профиль с индикатором загрузки · Ctrl + Enter
<div class="card">
  <button id="load">Загрузить профиль</button>
  <p id="status">Нажмите кнопку</p>
</div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

Практика: собираем дашборд

ЗАДАЧА 7.1

Дашборд пользователя

средняя

Напишите async function loadDashboard(id): она выводит «Загрузка...», затем получает пользователя и его заказы одновременно (Promise.all — они не зависят друг от друга, нужен только id) и выводит строку Привет, имя! Заказов: N, на сумму S сомони.

Можно редактировать · Ctrl + Enter
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const users = { 1: { name: "Мадина" }, 2: { name: "Фарход" } };
const orders = { 1: [120, 80, 45], 2: [] };

async function fakeFetchUser(id) {
  await wait(200);
  if (!users[id]) throw new Error("пользователь не найден");
  return users[id];
}
async function fakeFetchOrders(id) {
  await wait(150);
  return orders[id] ?? [];
}

// async function loadDashboard(id) { ... }

loadDashboard(1);
Ожидаемый результат
Загрузка...
Привет, Мадина! Заказов: 3, на сумму 245 сомони
Подсказка 1

const [user, list] = await Promise.all([fakeFetchUser(id), fakeFetchOrders(id)]);

Подсказка 2

Сумма заказов: list.reduce((total, price) => total + price, 0). Строку удобно собрать шаблонными кавычками.

ЗАДАЧА 7.2

Загрузка с ошибкой

сложная

Доработайте loadDashboard: если пользователь не найден, выводите Ошибка: … вместо приветствия. В любом случае, успех или ошибка, в конце выводится «Загрузка завершена». Запустите функцию для пользователей 1 и 99 по очереди (дождавшись первой).

Можно редактировать · Ctrl + Enter
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const users = { 1: { name: "Мадина" }, 2: { name: "Фарход" } };
const orders = { 1: [120, 80, 45], 2: [] };

async function fakeFetchUser(id) {
  await wait(200);
  if (!users[id]) throw new Error("пользователь не найден");
  return users[id];
}
async function fakeFetchOrders(id) {
  await wait(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. Перед каждым решением оставьте комментарий с номером задачи.

  1. Таймер на промисах. Напишите функцию delay(ms), которая возвращает промис, выполняющийся через ms миллисекунд. С её помощью выведите «3», «2», «1», «Старт!» с паузой 300 мс между строками (через цепочку then).
  2. Из колбэка в промис. Возьмите функцию checkAge(age, callback) из задачи 2.2 и напишите обёртку checkAgeAsync(age), которая возвращает промис. Вызовите её для двух возрастов, обработайте результат через then / catch.
  3. Цепочка из трёх шагов. Три шага «заказа пиццы» с задержкой: принять заказ → приготовить → доставить. Каждый возвращает промис со строкой. Выстройте их в плоскую цепочку и выведите результат каждого шага. Добавьте один catch в конце и проверьте, что он срабатывает, если заставить один шаг отклониться.
  4. Три загрузки одновременно. Три «запроса» с разной задержкой (курс сомони, погода в Душанбе, расписание группы). Запустите через Promise.all, выведите результаты и общее время (Date.now() до и после — оно должно быть около самой долгой задержки, а не суммы).
  5. async / await с ошибкой. Напишите async function loadProfile(id), которая «грузит» профиль 300 мс и для номера больше 100 бросает ошибку. Вызовите её для трёх номеров, ошибки ловите через try / catch / finally.
  6. Лист прогнозов. Придумайте три собственных фрагмента кода со смесью 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) → снова микрозадачи.
  • С ИИ: сначала сами, потом промпт по формуле. Объяснение ИИ про порядок вывода — всегда проверяем запуском.