Эффективные подходы к асинхронному коду на JavaScript

Раздел: JavaScript -> Асинхронное программирование в JavaScript

Асинхронное программирование в JavaScript

JavaScript является однопоточным языком, но благодаря асинхронным операциям может обрабатывать длительные задачи, не блокируя основной поток. Для работы с асинхронным кодом используются различные подходы, от простых колбэков до современного синтаксиса async/await. Каждый подход имеет свои цели и случаи использования.

Как писать асинхронный код линейно, без глубокой вложенности?

Самым эффективным решением является использование синтаксиса async/await, который позволяет писать асинхронный код, похожий на синхронный.

async function fetchData(url) {
  const response = await fetch(url);
  const data = await response.json();
  return data;
}

Async js (асинхронный javascript)

Ключевое слово await приостанавливает выполнение функции до получения результата промиса. Ошибки обрабатываются через try/catch.

Типичная ошибка: забыть обернуть await в try/catch, что приводит к необработанным ошибкам.

Решение: всегда использовать try/catch в async функциях или добавлять .catch() к вызову.

Цель: упрощение чтения и поддержки кода. Используется в большинстве современных проектов.

Как обрабатывать асинхронные операции без промисов?

Исторически первым способом были функции обратного вызова (колбэки). Они передаются как аргумент и вызываются после завершения операции.

function loadScript(src, callback) {
  const script = document.createElement('script');
  script.src = src;
  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error('Ошибка загрузки'));
  document.head.append(script);
}

Js promise (промисы в javascript)

Проблема: при большом количестве вложенных колбэков возникает так называемый callback hell.

Решение: переписать код с использованием промисов или async/await.

Цель: простой механизм для единичных асинхронных вызовов. Сегодня используется редко в новых проектах.

Как организовать последовательность асинхронных операций без вложенности?

Промисы позволяют строить цепочки с помощью метода .then() и обрабатывать ошибки через .catch().

fetch('/api/user')
  .then(response => response.json())
  .then(user => fetch(`/api/posts?userId=${user.id}`))
  .then(response => response.json())
  .then(posts => console.log(posts))
  .catch(err => console.error(err));

Типичная ошибка: забыть вернуть промис из .then(), что нарушает цепочку.

Решение: всегда использовать return или стрелочную функцию с явным возвратом.

Цель: избежать callback hell, улучшить читаемость. Подходит для последовательных зависимых запросов.

Как выполнить несколько независимых асинхронных операций параллельно?

Метод Promise.all() принимает массив промисов и возвращает промис, который завершается, когда все промисы разрешены. Результатом является массив результатов.

const [users, posts] = await Promise.all([
  fetch('/api/users').then(r => r.json()),
  fetch('/api/posts').then(r => r.json())
]);

Проблема: если хотя бы один промис отклоняется, весь Promise.all отклоняется (принцип "все или ничего").

Решение: использовать Promise.allSettled(), если нужно дождаться всех, независимо от успеха или ошибки.

Цель: ускорить выполнение путем параллельного запуска независимых задач. Часто используется для загрузки нескольких ресурсов.

Как дождаться первого успешного или отклоненного промиса?

Метод Promise.race() возвращает результат первого завершенного промиса (как успешного, так и отклоненного).

const winner = await Promise.race([
  fetch('/api/quick').then(r => r.json()),
  new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 3000))
]);

Проблема: если первый завершившийся промис отклонен, то Promise.race также отклоняется.

Решение: для ожидания первого успешного результата использовать Promise.any().

Цель: реализация таймаутов или выбор самого быстрого источника данных.

Как корректно обрабатывать ошибки в async функциях?

Внутри async функций ошибки обрабатываются с помощью конструкции try/catch. Если ошибка не перехвачена, она превращается в отклоненный промис.

async function loadUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) throw new Error('HTTP error');
    return await response.json();
  } catch (err) {
    console.error('Ошибка загрузки:', err);
    return null;
  }
}

Типичная ошибка: отсутствие try/catch в async функции, из-за чего исключение становится необработанным.

Решение: либо оборачивать тело функции в try/catch, либо добавлять .catch() к вызову async функции.

Цель: изолировать обработку ошибок внутри асинхронного кода и предотвратить падение приложения.

Подробные примеры с кодом и результатами

1. Последовательное выполнение с async/await

Пример получения пользователя и его постов с обработкой ошибок:

Пример
async function getUserWithPosts(userId) {
  try {
    const userResponse = await fetch(`/api/users/${userId}`);
    if (!userResponse.ok) throw new Error('Пользователь не найден');
    const user = await userResponse.json();
    const postsResponse = await fetch(`/api/posts?userId=${user.id}`);
    if (!postsResponse.ok) throw new Error('Ошибка загрузки постов');
    const posts = await postsResponse.json();
    return { user, posts };
  } catch (err) {
    console.error(err);
    return null;
  }
}

// Вызов
getUserWithPosts(1).then(result => console.log(result));

Вывод в консоль (при успешном ответе):

{
  user: { id: 1, name: "John Doe", email: "john@example.com" },
  posts: [
    { id: 101, title: "Post 1", body: "..." },
    { id: 102, title: "Post 2", body: "..." }
  ]
}

2. Параллельное выполнение с Promise.all и allSettled

Загрузка нескольких независимых ресурсов:

Пример
async function loadDashboardData() {
  const urls = [
    '/api/users',
    '/api/posts',
    '/api/comments'
  ];
  const promises = urls.map(url => fetch(url).then(r => r.json()));
  const [users, posts, comments] = await Promise.all(promises);
  return { users, posts, comments };
}

// Альтернатива с allSettled (не прерывается при ошибках)
async function loadWithFallback() {
  const results = await Promise.allSettled([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json())
  ]);
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`Ресурс ${index} загружен`, result.value);
    } else {
      console.warn(`Ресурс ${index} не загружен`, result.reason);
    }
  });
}

Результат Promise.all (если все успешно):

{
  users: [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }],
  posts: [{ id: 1, title: "First" }, { id: 2, title: "Second" }],
  comments: [{ id: 1, text: "Nice" }]
}

3. Цепочка промисов с обработкой ошибок

Пример последовательной трансформации данных:

Пример
function asyncDouble(n) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (typeof n !== 'number') reject(new Error('Не число'));
      else resolve(n * 2);
    }, 300);
  });
}

asyncDouble(5)
  .then(result => asyncDouble(result))
  .then(result => asyncDouble(result))
  .then(final => console.log('Итог:', final))
  .catch(err => console.error('Ошибка:', err.message));

Вывод:

Итог: 40

4. Promise.race для таймаута

Реализация ограничения времени выполнения запроса:

Пример
function fetchWithTimeout(url, timeout = 5000) {
  const fetchPromise = fetch(url).then(r => r.json());
  const timeoutPromise = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), timeout)
  );
  return Promise.race([fetchPromise, timeoutPromise]);
}

fetchWithTimeout('https://httpbin.org/delay/10', 2000)
  .then(data => console.log(data))
  .catch(err => console.error(err.message)); // Выведет 'Timeout' через 2 секунды

Результат:

Timeout

5. Генерация последовательных чисел с async/await и итераторами

Использование асинхронного генератора для получения данных по страницам:

Пример
async function* fetchPages(url, maxPages = 5) {
  let page = 1;
  while (page <= maxPages) {
    const response = await fetch(`${url}?page=${page}`);
    const data = await response.json();
    yield data;
    page++;
  }
}

(async () => {
  for await (const pageData of fetchPages('/api/items', 3)) {
    console.log('Получена страница:', pageData);
  }
})();

Результат (вывод последовательно):

Получена страница: { items: [...], page: 1 }
Получена страница: { items: [...], page: 2 }
Получена страница: { items: [...], page: 3 }

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

En
Async js (javascript)