async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
}
Кажется простым? Но когда ваш сервис растёт, а требования усложняются, этот лаконичный шаблон превращается в минное поле необработанных исключений, гонок ресурсов и неуправляемых зависимостей. Асинхронные операции — фундамент современных веб-приложений, но мало кто использует async/await за пределами поверхностного применения.
Анатомия асинхронных проблем
Рассмотрим типичные сбои в реальных проектах:
- Молчаливые падения
// Ошибка "съедается" без обработки
const user = await fetchUserData(userId);
updateUI(user);
- Последовательный ад
// Линейная цепочка замедляет работу
const profile = await loadProfile();
const posts = await loadPosts(); // Ожидание завершения loadProfile
const friends = await loadFriends(); // Ожидание предыдущих
- Неуправляемые операции
// Запрос нельзя отменить
async function search(query) {
const results = await fetchResults(query);
// Что если пользователь изменил запрос?
}
Эволюция инструментов управления
Обработка ошибок без try/catch ада
Использование try/catch для каждого асинхронного вызова создаёт шаблонный шум. Альтернатива — обёртка:
function safeAsync(promise) {
return promise
.then(data => [null, data])
.catch(error => [error, null]);
}
// На практике
const [userError, user] = await safeAsync(fetchUserData(123));
if (userError) {
handleError(userError);
return;
}
processUser(user);
Для TypeScript добавляем типизацию:
function safeAsync<T>(promise: Promise<T>): Promise<[Error | null, T | null]> {
return promise
.then<[null, T]>((data) => [null, data])
.catch<[Error, null]>((err) => [err instanceof Error ? err : new Error(String(err)), null]);
}
Параллелизм без боли
Распространённая антипаттерн — последовательные запросы:
// Медленный вариант
const a = await fetchA();
const b = await fetchB();
Решение — параллельное выполнение:
const [a, b] = await Promise.all([fetchA(), fetchB()]);
Но что если требуется более сложная логика? Используйте динамическое управление:
const controllers = new Map();
async function fetchWithCancel(id, signal) {
const resp = await fetch(`/data/${id}`, { signal });
return resp.json();
}
fetchItems(ids) {
ids.forEach(id => {
const controller = new AbortController();
controllers.set(id, controller);
fetchWithCancel(id, controller.signal);
});
}
// Отмена конкретного запроса
function cancelFetch(id) {
controllers.get(id)?.abort();
controllers.delete(id);
}
Отмена операций через AbortController
AbortController — фундамент для управления жизненным циклом запросов:
const searchController = new AbortController();
async function searchProducts(query) {
try {
const response = await fetch(`/search?q=${query}`, {
signal: searchController.signal
});
return response.json();
} catch (err) {
if (err.name === 'AbortError') {
console.log('Запрос отменён');
return [];
}
throw err;
}
}
// При изменении поискового запроса
function handleSearchChange(query) {
searchController.abort(); // Отмена предыдущего запроса
searchController = new AbortController();
searchProducts(query);
}
Для комплексной отмены цепочки операций:
function createCancellableAsync(task) {
const controller = new AbortController();
const promise = task(controller.signal);
return {
promise,
cancel: () => controller.abort()
};
}
// Использование
const { promise, cancel } = createCancellableAsync(async (signal) => {
const user = await fetchUser({ signal });
const orders = await fetchOrders(user.id, { signal });
return { user, orders };
});
// По событию отмены
cancelButton.addEventListener('click', cancel);
Расширенный параллелизм с управлением
Promise.all бросает ошибку при первом сбое — неприемлемо для независимых операций. Решение: Promise.allSettled
const results = await Promise.allSettled([
fetchConfig(),
fetchUserData(),
loadPreferences()
]);
const successfulResults = results
.filter(result => result.status === 'fulfilled')
.map(result => result.value);
const errors = results
.filter(result => result.status === 'rejected')
.map(result => result.reason);
Для управления скоростью очередь с ограниченным параллелизмом:
class TaskQueue {
constructor(concurrency) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
addTask(task) {
return new Promise((resolve, reject) => {
this.queue.push({
task,
resolve,
reject
});
this.next();
});
}
next() {
while (this.running < this.concurrency && this.queue.length) {
const { task, resolve, reject } = this.queue.shift();
this.running++;
task()
.then(resolve)
.catch(reject)
.finally(() => {
this.running--;
this.next();
});
}
}
}
// Использование: максимум 3 одновременных запроса
const apiQueue = new TaskQueue(3);
const results = await Promise.all([
apiQueue.addTask(() => fetch('/data/1')),
apiQueue.addTask(() => fetch('/data/2')),
// ... 10 задач
]);
Паттерны для цепочек зависимостей
Когда операции зависят друг от друга, чистая последовательность — не всегда оптимальное решение:
async function loadDashboard() {
const [userErr, user] = await safeAsync(fetchUser());
if (userErr) throw userErr;
const [prefsErr, prefs] = await safeAsync(fetchPrefs(user.id));
if (prefsErr) throw prefsErr;
const [notificationsErr, notifications] = await safeAsync(
fetchNotifications(user.id, prefs.lastSeen)
);
return { user, prefs, notifications };
}
Перепишем с эффекторным подходом:
const dashboardData = {
user: null,
prefs: null,
notifications: null
};
const fetchOperations = [
async () => { dashboardData.user = await fetchUser(); },
async () => {
if (!dashboardData.user) return;
dashboardData.prefs = await fetchPrefs(dashboardData.user.id);
},
async () => {
if (!dashboardData.prefs) return;
dashboardData.notifications = await fetchNotifications(
dashboardData.user.id,
dashboardData.prefs.lastSeen
);
}
];
await Promise.allSettled(fetchOperations.map(op => op()));
Границы разумного применения
Не всё требует асинхронных решений. Задумайтесь:
-
Стоит ли микрооптимизировать?
Для 1-3 последовательных вызовов разница с Promise.all() часто незначительна -
Гранулярность ошибок
groupFetch может скрывать критичные сбои среди второстепенных -
Статус выполнения
Добавьте прогресс-индикаторы для операций, занимающих >200мс:
const progress = new ProgressEmitter();
async function bulkUpload(files) {
progress.started(files.length);
await Promise.all(files.map(async (file, index) => {
await uploadFile(file);
progress.increment();
}));
progress.complete();
}
Интеграция с состоянием приложения
В React-приложении используйте хук для управляемых запросов:
function useAbortableFetch() {
const abortControllerRef = useRef(null);
useEffect(() => {
return () => abortControllerRef.current?.abort();
}, []);
return async (url, options) => {
abortControllerRef.current?.abort();
const controller = new AbortController();
abortControllerRef.current = controller;
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
return response.json();
} catch (err) {
if (err.name !== 'AbortError') throw err;
}
};
}
Корректное тестирование
Используйте временные моки для асинхронных тестов:
test('should cancel previous request', async () => {
const fetchSpy = jest.fn();
const search = createSearch(fetchSpy);
search('react');
search('angular'); // Отменяет "react"
await new Promise(resolve => setTimeout(resolve, 10));
expect(fetchSpy).toHaveBeenCalledTimes(2);
expect(fetchSpy.mock.calls[0][0].signal.aborted).toBe(true);
expect(fetchSpy.mock.calls[1][0].signal.aborted).toBe(false);
});
Итоговые рекомендации
-
Ошибки
Всегда обрабатывайте ошибки явно. Реализуйте стратегии retry для временных сбоев. -
Параллелизм
Используйте Promise.all и группируйте независимые запросы. Добавляйте ограничители при работе с лимитиями API. -
Отмена
Используйте AbortController для всех долгих операций. Подключайте к пользовательским событиям (перезапросы, покидание страницы). -
Зависимости
Для последовательного выполнения с зависимостями используйте явный синтаксис async/await без over-engineering. -
Отзывчивость
Предусматривайте показатели загрузки для действий, занимающих свыше 100 мс.
Управление асинхронностью требует не механического применения шаблонов, а понимания архтектурного контекста. Следуя этим принципам, вы создадите системы, устойчивые к сетевым сбоям, отзывчивые при взаимодействии и предсказуемые в поведении.