Эффективные подходы к асинхронному коду на 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 }