Async/Await: полное руководство по работе с асинхронным кодом

Разбираемся, как работать с асинхронным кодом с помощью async/await. Узнайте, как избежать callback hell, правильно обрабатывать ошибки, оптимизировать параллельное выполнение операций и применять продвинутые паттерны.

ОсновыJSРазработка

6 мин

Зачем нужна асинхронность

Представьте, что ваше приложение делает запрос к внешнему API, который отвечает 2 секунды. Без асинхронности программа просто зависнет на это время, блокируя выполнение других операций. Асинхронный код позволяет приложению продолжать работу, пока операция выполняется в фоне.

От callbacks к промисам и async/await

Эволюция асинхронного кода в JavaScript прошла несколько этапов:

Callback-функции (устаревший подход):

fetchUser(userId, (error, user) => {
  if (error) {
    console.error(error);
    return;
  }
  fetchPosts(user.id, (error, posts) => {
    if (error) {
      console.error(error);
      return;
    }
    // Callback hell начинается здесь
  });
});

Промисы (улучшение):

fetchUser(userId)
  .then(user => fetchPosts(user.id))
  .then(posts => console.log(posts))
  .catch(error => console.error(error));

Async/Await (современный подход):

async function getUserPosts(userId) {
  try {
    const user = await fetchUser(userId);
    const posts = await fetchPosts(user.id);
    return posts;
  } catch (error) {
    console.error(error);
  }
}

Основы async/await

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

async function fetchData() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  return data;
}

Важные правила:

  • await можно использовать только внутри async функций

  • async функция всегда возвращает промис

  • Если функция возвращает значение, оно автоматически оборачивается в Promise.resolve()

Обработка ошибок

Try-catch блоки — естественный способ обработки ошибок в async/await:

async function processOrder(orderId) {
  try {
    const order = await fetchOrder(orderId);
    const payment = await processPayment(order);
    const confirmation = await sendConfirmation(payment);
    return confirmation;
  } catch (error) {
    if (error.code === 'PAYMENT_FAILED') {
      await refundOrder(orderId);
    }
    throw new Error(`Order processing failed: ${error.message}`);
  }
}

Можно комбинировать с .catch() для более гранулярной обработки:

async function getData() {
  const data = await fetchData().catch(error => {
    console.error('Fetch failed:', error);
    return getDefaultData(); // Возвращаем данные по умолчанию
  });
  return data;
}

Параллельное выполнение

Одна из частых ошибок — последовательное выполнение независимых операций:

// Плохо: операции выполняются последовательно (6 секунд)
async function loadData() {
  const users = await fetchUsers(); // 3 секунды
  const posts = await fetchPosts(); // 3 секунды
  return { users, posts };
}

Используйте Promise.all() для параллельного выполнения:

// Хорошо: операции выполняются параллельно (3 секунды)
async function loadData() {
  const [users, posts] = await Promise.all([
    fetchUsers(),
    fetchPosts()
  ]);
  return { users, posts };
}

Promise.allSettled()

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

async function loadAllData() {
  const results = await Promise.allSettled([
    fetchUsers(),
    fetchPosts(),
    fetchComments()
  ]);
  
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`Operation ${index} succeeded:`, result.value);
    } else {
      console.error(`Operation ${index} failed:`, result.reason);
    }
  });
}

Promise.race()

Для операций с таймаутом:

async function fetchWithTimeout(url, timeout = 5000) {
  const timeoutPromise = new Promise((_, reject) => 
    setTimeout(() => reject(new Error('Request timeout')), timeout)
  );
  
  return Promise.race([
    fetch(url),
    timeoutPromise
  ]);
}

Async/await в циклах

Будьте осторожны с циклами — они могут создать неожиданное поведение:

// Последовательная обработка
async function processSequentially(items) {
  for (const item of items) {
    await processItem(item); // Ждем завершения каждой операции
  }
}

// Параллельная обработка
async function processInParallel(items) {
  await Promise.all(items.map(item => processItem(item)));
}

// Пакетная обработка
async function processBatches(items, batchSize = 5) {
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    await Promise.all(batch.map(item => processItem(item)));
  }
}

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

1. Забытый await

// Ошибка: функция вернет промис, а не данные
async function getData() {
  return fetchData(); // Забыли await
}

// Правильно
async function getData() {
  return await fetchData();
}

2. Использование await в forEach

// Не работает: forEach не понимает async
items.forEach(async (item) => {
  await processItem(item);
});

// Используйте for...of
for (const item of items) {
  await processItem(item);
}

3. Создание промисов в цикле без контроля

// Создаст тысячи одновременных запросов
const promises = items.map(item => fetchItem(item));
await Promise.all(promises);

// Лучше контролировать параллелизм

Продвинутые паттерны

Retry с экспоненциальной задержкой:

async function fetchWithRetry(url, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fetch(url);
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      const delay = Math.pow(2, i) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

Кэширование результатов:

const cache = new Map();

async function fetchWithCache(key, fetcher) {
  if (cache.has(key)) {
    return cache.get(key);
  }
  const result = await fetcher();
  cache.set(key, result);
  return result;
}

Debounce для async функций:

function asyncDebounce(func, wait) {
  let timeout;
  return function(...args) {
    return new Promise((resolve) => {
      clearTimeout(timeout);
      timeout = setTimeout(async () => {
        resolve(await func.apply(this, args));
      }, wait);
    });
  };
}

Async/await в других языках

Концепция async/await существует не только в JavaScript:

Python:

async def fetch_data():
    response = await aiohttp.get('https://api.example.com')
    return await response.json()

C#:

async Task<string> FetchDataAsync() {
    var response = await httpClient.GetAsync("https://api.example.com");
    return await response.Content.ReadAsStringAsync();
}

Rust:

async fn fetch_data() -> Result<String, Error> {
    let response = reqwest::get("https://api.example.com").await?;
    Ok(response.text().await?)
}

Производительность и best practices

1. Избегайте излишнего await

// Неоптимально
async function process() {
  const result = await computeValue();
  return result; // Лишний await
}

// Лучше
async function process() {
  return computeValue();
}

2. Используйте параллелизм где возможно

// Медленно
const user = await getUser();
const posts = await getPosts();

// Быстрее
const [user, posts] = await Promise.all([getUser(), getPosts()]);

3. Добавляйте таймауты для внешних запросов

async function safeFetch(url) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 5000);
  
  try {
    const response = await fetch(url, { signal: controller.signal });
    return response;
  } finally {
    clearTimeout(timeout);
  }
}

Заключение

Async/await делает асинхронный код читаемым и понятным, устраняя проблемы callback hell и упрощая обработку ошибок. Ключевые моменты:

  • Используйте async/await для последовательных операций

  • Применяйте Promise.all() для параллельного выполнения

  • Не забывайте обрабатывать ошибки через try-catch

  • Будьте осторожны с циклами и всегда используйте await где нужно

  • Контролируйте параллелизм для избежания перегрузки системы

Практика и понимание того, когда операции должны выполняться последовательно, а когда параллельно, помогут вам писать эффективный асинхронный код.

Кодик предлагает структурированные курсы по JavaScript, Python, C, Rust и многим другим языкам, где вы сможете изучить асинхронное программирование от основ до продвинутых паттернов. Интерактивные уроки с практическими заданиями помогут закрепить материал на реальных примерах.

Остались вопросы по async/await или столкнулись со сложной задачей? Присоединяйтесь к нашему Telegram-каналу, где опытные разработчики и активное сообщество всегда готовы помочь разобраться с любой проблемой — от синтаксических тонкостей до архитектурных решений. Задавайте вопросы, делитесь опытом и учитесь вместе с сообществом!

Комментарии