Async/Await в JavaScript: паттерны, которые сэкономят часы

#javascript#async#frontend

Когда в 2017-м async/await добрались до production-сборок, это казалось синтаксическим сахаром над промисами. Но за пять лет паттерны эволюционировали — теперь это полноценный инструмент для работы с асинхронными потоками. Разберу три сценария, где современные подходы реально сокращают когнитивную нагрузку.

Top-level await: когда IIFE надоел

Раньше для использования await приходилось оборачивать код в Immediately Invoked Function Expression:

(async () => {
  const config = await fetchConfig();
  initApp(config);
})();

С ES2022 и модулями (.mjs или type=“module”) можно писать await на верхнем уровне:

// config.mjs
export const config = await fetch('/config.json').then(r => r.json());

// app.mjs
import { config } from './config.mjs';
console.log(config.apiUrl); // Уже разрешённый объект

Где ловушка: модуль с top-level await блокирует загрузку зависимостей. Если config.mjs тормозит, все импортирующие его модули ждут. В React/Vue такое лучше не использовать для динамических данных — только для статической конфигурации.

For-await-of: стримы без колбэков

Представьте, что вы читаете ленту Twitter через API с пагинацией. Классический подход с while и await:

let cursor = null;
let allTweets = [];
do {
  const res = await fetch(`/api/tweets?cursor=${cursor}`);
  const { tweets, nextCursor } = await res.json();
  allTweets.push(...tweets);
  cursor = nextCursor;
} while (cursor);

С async generators и for-await-of код становится линейным:

async function* fetchTweets() {
  let cursor = null;
  do {
    const res = await fetch(`/api/tweets?cursor=${cursor}`);
    const { tweets, nextCursor } = await res.json();
    yield tweets;
    cursor = nextCursor;
  } while (cursor);
}

// Использование
for await (const tweets of fetchTweets()) {
  renderBatch(tweets);
}

Что под капотом: for-await-of работает с любым объектом, у которого есть Symbol.asyncIterator. Поддерживается в Node.js стримах, WebSocket, EventSource.

Promise.allSettled: graceful degradation по-современному

Допустим, у вас дашборд с виджетами. Если один API упал, остальные должны продолжать работать. До 2020-го писали костыли с .catch:

const [users, posts] = await Promise.all([
  fetchUsers().catch(() => []),
  fetchPosts().catch(() => [])
]);

Теперь можно явно обрабатывать статусы:

const results = await Promise.allSettled([
  fetchUsers(),
  fetchPosts(),
  fetchAnalytics()
]);

const data = {
  users: results[0].status === 'fulfilled' ? results[0].value : [],
  posts: results[1].status === 'fulfilled' ? results[1].value : [],
  analytics: results[2].status === 'fulfilled' ? results[2].value : null
};

Производительность: allSettled не быстрее all — просто другая семантика. Не используйте для последовательных запросов, только для параллельных независимых операций.

Комбинированный пример: дашборд с live-данными

Соединим все три подхода в реальном сценарии:

// config.mjs
export const config = await fetch('/config').then(r => r.json());

// api.mjs
export async function* streamMetrics() {
  const es = new EventSource('/live');
  for await (const event of es) {
    yield JSON.parse(event.data);
  }
}

// dashboard.mjs
import { config } from './config.mjs';
import { streamMetrics } from './api.mjs';

const [userData, postsData] = await Promise.allSettled([
  fetch(`/users/${config.userId}`),
  fetch(`/posts/${config.userId}`)
]);

renderStatic({
  user: userData.status === 'fulfilled' ? await userData.value.json() : null,
  posts: postsData.status === 'fulfilled' ? await postsData.value.json() : []
});

for await (const metric of streamMetrics()) {
  updateChart(metric);
}

Что это даёт:

  1. Конфиг загружается до инициализации приложения
  2. Статические данные обрабатываются с учётом возможных ошибок
  3. Live-данные приходят без вложенных колбэков

Когда не использовать

Async/await — не серебряная пуля. Вот где он добавляет сложности:

Попробуйте переписать один из своих модулей с этими паттернами. Особенно хорошо они работают в связке с TypeScript — подсказки типов помогают не запутаться в статусах Promise.allSettled.


Попробуй сам: Cursor — AI-редактор для разработчиков.


Источник: https://dev.to/timevolt/javascript-asyncawait-patterns-that-will-save-you-hours-the-matrix-reloaded-edition-1mnl