Объяснение промисов в JavaScript: then, catch, Promise.all и распространенные ошибки

  1. Главная
  2. Блог
  3. Разработка программного обеспечения

Разработка программного обеспечения

Объяснение промисов в JavaScript: then, catch, Promise.all и распространенные ошибки

Промисы — это то, как JavaScript обрабатывает работу, которая завершается позже, и поверх них строится async/await. Узнайте, как они ведут себя на самом деле, с реальными результатами каждого примера, и избегайте ошибок, которые поглощают ваши ошибки.

10 мин чтения
Объяснение промисов в JavaScript: then, catch, Promise.all и типичные ошибки

Главное

  • обещание — это объект, представляющий результат, который будет доступен позже.
  • Оно отложено до тех пор, пока не будет выполнено или отклонено, и однажды урегулированное оно никогда не меняется.
  • Каждый then возвращает новое обещание, что делает возможным цепочку.
  • Используйте Promise.all, когда требуется каждый результат, и Promise.allSettled, когда вам нужен каждый результат.

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

Даже если вы в основном пишете async и await, вы используете промисы внизу, поэтому их понимание значительно упрощает отладку. Мы запускали эти примеры на Node.js 24, и показанный результат получен в результате этого запуска. В современных браузерах они ведут себя так же.

Что такое обещание?

Обещание — это заполнитель для будущего значения. Подумайте о звонке, который вам вручают в ресторане, когда вы делаете заказ. У вас еще нет еды, но вы держите в руках что-то, что подскажет вам, когда она будет готова или что возникла проблема, а тем временем вы можете заниматься другими делами.

В коде асинхронная функция немедленно возвращает обещание, и вы добавляете, что должно произойти дальше:

fetch("/api/user")
  .then((response) => response.json())
  .then((user) => console.log(user.name))
  .catch((error) => console.log("Failed:", error.message));

Три обещания гласят

Государство Значение Что работает
Ожидается Работа не закончена Пока ничего
Выполнено Он закончился со значением then обработчики
Отклонено Это не удалось по причине catch обработчики

Обещание, которое выполнено или отклонено, называется выполненным. Обещание может быть выполнено только один раз. После этого его состояние и значение фиксируются, а последующие вызовы resolve или reject игнорируются.

Создать обещание

Обычно вы используете обещания из API, таких как fetch, но создание одного показывает, как они работают. Конструктор получает две функции: resolve и reject:

function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) {
        resolve({ id, name: "Asha" });
      } else {
        reject(new Error("Invalid id"));
      }
    }, 100);
  });
}

console.log(fetchUser(1));

// Promise { <pending> }

Немедленная регистрация обещания показывает, что оно все еще находится в состоянии ожидания, поскольку таймер не сработал. Значение может быть достигнуто только через обработчик или await. Всегда отклоняйте объект Error, а не строку, поскольку Error содержит трассировку стека, которая помогает при отладке.

затем поймай и, наконец,

Эти три метода присоединяют обработчики к обещанию:

fetchUser(1).then((user) => console.log(user.name));
// Asha

fetchUser(-1)
  .then((user) => console.log("never runs", user))
  .catch((error) => console.log("caught:", error.message))
  .finally(() => console.log("finally"));

// caught: Invalid id
// finally

then выполняется в случае успеха, catch выполняется в случае сбоя и finally выполняется в любом случае. Это делает finally подходящим местом для очистки, например, для скрытия индикатора загрузки. Он не получает аргументов, поскольку его не должен волновать результат.

Объединение обещаний

Ключевая идея заключается в том, что then всегда возвращает новое обещание. Все, что возвращает ваш обработчик, становится значением нового обещания:

Promise.resolve(2)
  .then((n) => n * 10)
  .then((n) => n + 1)
  .then((n) => console.log("chain:", n));

// chain: 21

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

getUser(id)
  .then((user) => getOrders(user.id))
  .then((orders) => getInvoice(orders[0].id))
  .then((invoice) => console.log(invoice.total))
  .catch(handleError);

Как ошибки перемещаются по цепочке

Отклоненное обещание пропускает каждый then, пока не достигнет catch. Выброс внутри обработчика также считается отклонением. И возвращаемый catch обычно возвращает цепочку в нужное русло:

Promise.resolve("start")
  .then(() => {
    throw new Error("step 2 failed");
  })
  .then(() => console.log("skipped"))
  .catch((error) => {
    console.log("caught:", error.message);
    return "recovered";
  })
  .then((value) => console.log("continues:", value));

// caught: step 2 failed
// continues: recovered

Думайте об этом как об асинхронной версии try и catch. Один catch в конце обычно охватывает всю цепочку. Если вы хотите зарегистрировать только ошибку и все равно потерпеть неудачу, создайте ее еще раз изнутри catch.

Совет: fetch отклоняется только при сбоях в сети. Ответ 404 или 500 по-прежнему выполняется, поэтому проверьте response.ok самостоятельно. В нашей шпаргалке по кодам статуса HTTP объясняется, что означает каждый код.

Promise.all, allSettled, раса и любые

Эти статические методы координируют сразу несколько промисов. Все они принимают массив и возвращают одно обещание.

Метод Выполняется, когда Отклоняет, когда Используйте его для
Promise.all Все выполнить Любой отвергает Все результаты обязательны
Promise.allSettled Все решено Никогда Вы хотите каждого результата
Promise.race Первый, кто поселяется, выполняет Первый, кто решает проблемы с браком Таймауты
Promise.any Первый выполняет Все отклонить Первый успешный источник

Чтобы протестировать их, мы использовали два небольших помощника: один завершается успешно после задержки, а другой завершается сбоем после задержки:

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.all

await Promise.all([wait(300, "a"), wait(100, "b"), wait(200, "c")]);

// [ 'a', 'b', 'c' ]   took about 300 ms

Две детали, на которые стоит обратить внимание. Результаты сохраняют порядок ввода, хотя b закончил первым. И общее время составило около 300 мс, время самой медленной задачи, а не сумма 600 мс, потому что все три уже работали одновременно. Если кто-нибудь из них отвергнет, то сразу отвергнет все:

await Promise.all([wait(100, "a"), fail(50, "b failed")]);

// rejected: b failed

Promise.allSettled

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

const results = await Promise.allSettled([wait(50, "a"), fail(50, "b failed")]);
results.map((r) => r.status);

// [ 'fulfilled', 'rejected' ]

Promise.race и Promise.any

await Promise.race([wait(300, "slow"), wait(100, "fast")]);   // fast
await Promise.any([fail(50, "x"), wait(100, "ok")]);          // ok

Разница в том, что race принимает решение первым, даже если оно не удалось, тогда как any игнорирует неудачи, пока что-то удается. Классическое использование race — добавление тайм-аута к медленному запросу, хотя более медленная задача продолжает выполняться в фоновом режиме, пока вы не отмените ее.

Обещания и async/await

async и await — более читабельный способ работы с теми же промисами. Функция async всегда возвращает обещание, а await приостанавливает выполнение этой функции до тех пор, пока обещание не будет выполнено:

async function showInvoice(id) {
  try {
    const user = await getUser(id);
    const orders = await getOrders(user.id);
    console.log(orders.length);
  } catch (error) {
    console.log("Failed:", error.message);
  }
}

Отклонения становятся выброшенными ошибками, поэтому работают обычные try и catch. Вам все равно понадобится Promise.all для параллельной работы, потому что ожидание одного вызова за другим запускает их последовательно. В нашем руководстве по асинхронному/ожидающему использованию JavaScript эта тема рассматривается более подробно.

Обещания и цикл событий

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

setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("microtask"));
console.log("sync");

// sync
// microtask
// timeout

Синхронный код выполняется первым. Далее идет очередь микрозадач, в которой хранятся обработчики обещаний. Только после этого цикл событий выбирает следующую задачу, например таймер. Это популярный вопрос на собеседованиях, и он объясняет, почему нулевой результат setTimeout не является немедленным.

Обещания против обратных вызовов

Беспокойство Обратные вызовы Обещания
Секвенирование Вложенные, глубже на каждом этапе Плоская цепь
Ошибки Проверено на каждом уровне Один catch может перекрыть цепочку
Параллельная работа Ручные счетчики Promise.all
Позвонили дважды по ошибке? Возможно Невозможно рассчитаться дважды

Распространенные ошибки

  • Забываем вернуться внутрь then. Без return следующий шаг получает undefined и не ждет вашего внутреннего обещания.
  • Нет catch вообще. Это приводит к необработанному отклонению, а в Node.js это останавливает процесс по умолчанию.
  • Вложение вместо цепочки. Помещение then внутрь then воссоздает ад обратного вызова. Верните обещание и продолжите цепочку.
  • Обертывание обещания в new Promise. Если функция уже возвращает обещание, просто используйте его.
  • Использование forEach с асинхронными обратными вызовами. Он не ждет. Используйте for...of или Promise.all с map, как показано в нашем руководстве по методам массивов JavaScript.
  • Думаю, что обещание можно отменить. Само по себе оно не может быть отменено. Для запросов используйте AbortController.

Итоги

Промисы сводятся к нескольким правилам: обещание выполняется один раз, then возвращает новое обещание, а ошибки перемещаются по цепочке, пока что-то их не перехватит. Как только эти правила станут естественными, async и await перестанут быть волшебством. Для практики перепишите одну функцию обратного вызова, используя промисы, затем получите две конечные точки с помощью Promise.all и правильно обработайте ошибку.

Часто задаваемые вопросы

Что такое обещание в JavaScript?

Обещание — это объект, представляющий конечный результат асинхронной работы. Он начинается как ожидающий, затем либо выполняется со значением, либо отклоняется с указанием причины.

В чем разница между thenи catch?

затем запускается, когда обещание выполняется, и catch запускается, когда оно отклоняется. В этом случае catch фактически использует только обработчик отклонения, а также может перехватывать ошибки, возникшие на более ранних этапах.

В чем разница между Promise.all и Promise.allSettled?

Promise.all отклоняется, как только отклоняется одно обещание. Promise.allSettled всегда ожидает их всех и сообщает о статусе, значении или причине каждого из них.

Должен ли я использовать обещания или async/await?

Они работают вместе, поскольку ожидание основано на обещаниях. Большая часть кода лучше всего читается с помощью async/await, тогда как Promise.all и подобные методы остаются полезными для параллельной работы.

Обещания выполняются параллельно?

JavaScript выполняет по одному фрагменту кода в потоке, но работает так, как будто сетевые запросы выполняются в фоновом режиме. Несколько из них могут выполняться одновременно.

Продолжайте учиться

Работайте с Stack Quora

Нужна помощь с тестированием, WordPress или SEO?

Мы помогаем командам выпускать лучшее программное обеспечение и развивать свои сайты. Расскажите нам, над чем вы работаете, и мы предложим четкий следующий шаг.

Продолжайте читать

Больше руководств по тестирование, искусственный интеллект и современные разработки

Понравилась эта статья? Выберите тему и сразу приступайте к нашим новейшим руководствам.

Искусственный интеллект

Объяснение галлюцинаций искусственного интеллекта: почему языковые модели все выдумывают и как их уменьшить

Галлюцинации ИИ объяснили: что это такое, почему языковые модели их создают, распространенные типы, как уменьшить их с помощью заземления и подсказо…

Читать статью →
DevOps

Объяснение переменных среды: файлы .env, секреты и правильно выполненная конфигурация

Переменные среды объясняются на примерах Python, которые мы запускали: их чтение и установка, почему каждое значение является строкой, файлы .env, н…

Читать статью →
Разработка ПО

Объяснение ограничения скорости API: алгоритмы, заголовки и обработка ошибок 429

Ограничение скорости API объясняется с помощью проведенного нами моделирования: сегмент токенов, фиксированное окно, скользящее окно, 429 ответов, з…

Читать статью →
Автоматизация тестирования

Издевательство в Python: unittest.mock, патч и ошибки, из-за которых тесты лгут

Издевательство на Python с помощью unittest.mock, протестировано: Mock и return_value, Side_effect, исправление использования имени, спецификация и …

Читать статью →
Тестирование ПО

Объяснение покрытия кода: покрытие линий или ветвей и что на самом деле означает 100%

Покрытие кода объяснено с помощью реального запуска покрытия.py: покрытие строк, ветвей и условий, чтение отчета, почему 100%-ное покрытие все еще м…

Читать статью →
Базы данных и SQL

DELETE, TRUNCATE и DROP в SQL: различия, откат и когда использовать каждый из них

Объяснение DELETE, TRUNCATE и DROP на примере протестированного SQL: что каждый из них удаляет, откат, триггеры, сброс идентификаторов, скорость и ч…

Читать статью →
Базы данных и SQL

Объяснение представлений SQL: создавайте, используйте и избегайте ловушек, с примерами

Представления SQL, объясненные с помощью запросов, которые мы выполняли: создание представления, результаты в реальном времени, скрытие столбцов, пр…

Читать статью →
Разработка ПО

Интерфейс TypeScript и тип: реальные различия, протестированные с помощью компилятора

Интерфейс TypeScript против типа, проверенный реальным компилятором: что могут делать оба, что может делать только один, слияние объявлений, расшире…

Читать статью →
Разработка ПО

Объяснение Python *args и **kwargs: гибкие функции с понятными примерами

Python *args и **kwargs, объясненные с помощью кода, который мы запускали: упаковка и распаковка, порядок параметров, аргументы только по ключевым с…

Читать статью →
Разработка ПО

Лямбда-функции Python: синтаксис, реальное использование и когда их не следует использовать

Лямбда-функции Python, объясненные с помощью кода, который мы запускали: синтаксис, сортировка с помощью ключа, карты и фильтра, замыкания, ловушка …

Читать статью →
Искусственный интеллект

Что такое ИИ-агенты? Как они работают: можно запустить простой цикл агента

Что такое агенты ИИ? Простое руководство по циклу агента, инструментам, памяти и ограничениям, с небольшим циклом Python, который мы запустили, реал…

Читать статью →
DevOps

Мониторинг и наблюдаемость: реальная разница с журналами, метриками и трассировками

Объяснение мониторинга и наблюдаемости: известные и неизвестные сбои, журналы, метрики и трассировки, пошаговое руководство по инцидентам при оформл…

Читать статью →
DevOps

Docker Volumes против Bind Mounts: правильное сохранение данных контейнера

Объяснение томов Docker и монтирования привязки: почему исчезают данные контейнера, именованные тома, монтирование привязки, tmpfs, примеры создания…

Читать статью →
Автоматизация тестирования

TDD против BDD: ключевые различия, одна функция, созданная для обоих способов, и когда использовать каждый

TDD и BDD объясняются с помощью одной функции скидки, созданной в обоих направлениях: красно-зеленый рефакторинг с кодом, который мы запускали, сцен…

Читать статью →
Тестирование ПО

Пользовательское приемочное тестирование (UAT): процесс, контрольный список и реальные примеры

Приемочное тестирование пользователей объяснило: кто его проводит, чем оно отличается от контроля качества, семиэтапный процесс, шаблон тестового сц…

Читать статью →
Базы данных и SQL

Объяснение значений NULL в SQL: IS NULL, COALESCE, NULLIF и ловушки, которых следует избегать

Значения SQL NULL объяснены с помощью запросов, которые мы выполнили: трехзначная логика, IS NULL, COALESCE, NULLIF, как агрегаты, соединения и сорт…

Читать статью →
Базы данных и SQL

Оператор SQL CASE WHEN: синтаксис, реальные примеры и распространенные ошибки

SQL CASE WHEN объяснен с помощью запросов, которые мы выполняли: простые формы и формы для поиска, порядок условий, повороты с помощью SUM, трюки CO…

Читать статью →
Разработка ПО

Что такое JWT? Веб-токены JSON, объясненные на рабочем примере

Что такое JWT? Объяснение JSON Web Tokens: три части: токен, созданный и измененный в коде, который мы запускали, JWT и сеансы, хранилище, токены об…

Читать статью →
Разработка ПО

Объяснение цикла событий JavaScript: стек вызовов, микрозадачи и таймеры с примерами

Цикл событий JavaScript объясняется с помощью кода, который мы запускали: стек вызовов, очереди задач и микрозадач, почему setTimeout 0 не является …

Читать статью →
Разработка ПО

Генераторы Python и объяснение доходности: экономьте память с помощью реальных примеров

Генераторы Python и доходность, объясненные с помощью кода, который мы запускали: как они приостанавливаются и возобновляются, измеряют экономию пам…

Читать статью →
Искусственный интеллект

Что такое вложения? Простое объяснение векторов и косинусного сходства

Что такое вложения? Простое руководство по векторам, косинусному подобию и семантическому поиску с небольшим примером, который мы использовали, общи…

Читать статью →
DevOps

Сине-зеленое и канареечное развертывание: различия, компромиссы и как выбрать

Объяснение сине-зеленого и канареечного развертывания: как каждый из них работает, стоимость, риски и откат, стабильное группирование пользователей,…

Читать статью →
Автоматизация тестирования

Ненадежные тесты: что их вызывает и как их исправить навсегда

Ненадежные тесты объясняются на демонстрациях, которые мы запускали: основные причины, способы подтверждения и диагностики, правильные исправления в…

Читать статью →
Тестирование ПО

Нагрузочное тестирование и стресс-тестирование: различия, метрики и примеры k6

Объяснение нагрузочного и стресс-тестирования: что доказывает каждое из них, тесты на всплески и выдержки, важные метрики, почему средние значения в…

Читать статью →
Тестирование ПО

Тестовый пример и тестовый сценарий: ключевые различия, примеры и когда использовать каждый

Объяснение тестового случая и тестового сценария: четкие определения, параллельная таблица, одна функция сброса пароля, взятая из сценариев в случаи…

Читать статью →
Базы данных и SQL

Оптимизация SQL-запросов: чтение планов выполнения и исправление медленных запросов с указанием времени

Оптимизация SQL-запросов проверена на миллионе строк: читайте планы выполнения, добавляйте правильные индексы, исправляйте неразмещаемые фильтры, ис…

Читать статью →
Базы данных и SQL

SQL UNION против UNION ALL: различия, примеры и проверка производительности

SQL UNION и UNION ALL объяснены с помощью запросов, которые мы выполнили: дубликаты, правила, которым следуют оба, реальная ошибка данных, измеренна…

Читать статью →
Разработка ПО

Учебное пособие по API JavaScript Fetch: GET, POST, обработка ошибок и тайм-ауты

Учебное пособие по API JavaScript Fetch, протестированное на локальном сервере: GET и POST, почему не выдается ошибка 404, многоразовый помощник, ст…

Читать статью →
Разработка ПО

Обработка файлов Python: чтение, запись, CSV, JSON и pathlib с примерами

Обработка файлов Python объясняется с помощью кода, который мы запускали: открытые режимы, чтение больших файлов, CSV, JSON, pathlib, двоичные файлы…

Читать статью →
Разработка ПО

Классы и объекты Python: учебник ООП с реальными примерами

Классы и объекты Python, объясненные с помощью кода, который мы запускали: __init__, методы, атрибуты класса и экземпляра, наследование, свойства, м…

Читать статью →
Искусственный интеллект

Что такое большая языковая модель? Как работают LLM, объяснено без шумихи

Что такое большая языковая модель? LLM объясняется простым языком: токены, встраивания, внимание, обучение, температурные и контекстные окна, крошеч…

Читать статью →
DevOps

Команды Linux для начинающих: основное руководство по терминалу с примерами

Команды Linux для начинающих, сгруппированные по задачам: навигация, файлы, просмотр журналов, grep, каналы, разрешения, процессы и архивы, с практи…

Читать статью →
Автоматизация тестирования

Selenium vs Playwright: ключевые различия, сравнение кода и что выбрать

Сравнение Selenium и Playwright: архитектура, ожидание, поддержка браузера и языка, отладка, одинаковый тест в обоих инструментах, советы по миграци…

Читать статью →
Тестирование ПО

Разделение эквивалентности: метод проектирования тестов, объясненный на рабочих примерах

Объяснение эквивалентного разделения: как найти действительные и недействительные разделы, рабочие примеры для возрастов, паролей и купонов, проведе…

Читать статью →
Тестирование ПО

Серьезность и приоритет в тестировании программного обеспечения: различия, матрица и реальные примеры

Объяснение серьезности и приоритета при тестировании программного обеспечения: четкие определения, четырехсторонняя матрица с примерами, шкалы уровн…

Читать статью →
Базы данных и SQL

Учебное пособие по подзапросам SQL: типы, коррелирующие подзапросы и EXISTS с примерами

Руководство по подзапросам SQL с выполненными нами запросами: скалярные подзапросы, IN, FROM и SELECT, коррелированные подзапросы, EXISTS, ловушка N…

Читать статью →
Базы данных и SQL

Объяснение транзакций SQL и ACID: COMMIT, ROLLBACK и уровни изоляции

Транзакции SQL и ACID, объясненные с помощью банковского перевода, который мы выполнили: COMMIT, ROLLBACK, точки сохранения, блокировки, уровни изол…

Читать статью →
Разработка ПО

Git Commands Cheat Sheet: The Commands You Actually Use, With Examples

Шпаргалка по командам Git, сгруппированная по задачам: настройка, промежуточный этап, ветки, удаленные устройства, тайник, история и исправление оши…

Читать статью →
Разработка ПО

Объяснение промисов в JavaScript: then, catch, Promise.all и распространенные ошибки

Обещания JavaScript, объясненные с помощью кода, который мы запускали: состояния, цепочки then и catch, обработка ошибок, Promise.all, allSettled, R…

Читать статью →
Разработка ПО

Учебное пособие по словарю Python: методы, примеры и общие шаблоны

Учебное пособие по словарю Python с кодом, который мы выполнили: создание, чтение и обновление словарей, ключевые методы, слияние, понимание, шаблон…

Читать статью →
Искусственный интеллект

Что такое РАГ? Генерация с расширенным поиском, объясненная на рабочем примере

Что такое РАГ? Retrieval-augmented generation explained step by step: indexing, chunking, embeddings, retrieval, a small working Python example, eva…

Читать статью →
DevOps

Учебное пособие по Terraform для начинающих: инфраструктура как код от первого файла до модулей

Учебное пособие по Terraform для начинающих: начните без облачной учетной записи, изучите инициализацию, планирование и применение, затем переменные…

Читать статью →
DevOps

Docker и Kubernetes: ключевые различия, как они работают вместе и что вам нужно

Объяснение Docker и Kubernetes: что делает каждый из них, одно и то же приложение работает с обоими, сравнение Docker Compose и Swarm, изменение doc…

Читать статью →
Автоматизация тестирования

Учебное пособие по Pytest для начинающих: фикстуры, параметризация и реальные примеры

Учебное пособие по pytest для начинающих с тестами, которые мы запускали на pytest 9.2: утверждения, фикстуры, параметризация, conftest, маркеры, tm…

Читать статью →
Тестирование ПО

Жизненный цикл тестирования программного обеспечения (STLC): 6 этапов, критерии входа и выхода, пример

Объясняется жизненный цикл тестирования программного обеспечения: шесть этапов STLC с критериями входа и выхода, результаты, пример проработанной фу…

Читать статью →
Разработка ПО

Git merge vs rebase: отличия, примеры и когда что использовать

Git merge vs rebase с реальным выводом log, сравнительной таблицей, золотым правилом rebase и безопасным рабочим процессом для команды.

Читать статью →
Разработка ПО

Генераторы списков в Python: синтаксис, примеры и когда их избегать

Генераторы списков в Python: синтаксис, фильтры, if-else, вложенные циклы, измеренное время и случаи, когда обычный цикл for лучше.

Читать статью →
Базы данных и SQL

SQL JOIN: объяснение с примерами (Inner, Left, Right, Full)

Разберите SQL JOIN шаг за шагом на примерах таблиц и запросов: INNER, LEFT, RIGHT, FULL, CROSS и SELF, а также частые ошибки и советы по производите…

Читать статью →