Asynchroniczność w JavaScript: Promise, async/await

JavaScript jest językiem jednowątkowym, ale dzięki asynchroniczności może wykonywać operacje nieblokujące. W tym artykule poznamy ewolucję obsługi asynchroniczności w JavaScript - od callbacków, przez Promise, po nowoczesną składnię async/await.

Dlaczego asynchroniczność jest ważna?

W aplikacjach webowych często potrzebujemy wykonywać operacje, które mogą zająć dużo czasu:

Bez asynchroniczności, aplikacja byłaby "zamrożona" podczas takich operacji, co znacznie pogorszyłoby doświadczenie użytkownika.

Era callbacków

Pierwszy sposób obsługi asynchroniczności w JavaScript to callbacks - funkcje przekazywane jako argumenty, które są wywoływane po zakończeniu operacji.

// Przykład prostego callbacka function fetchUserData(userId, callback) { setTimeout(() => { const userData = { id: userId, name: 'Jan Kowalski', email: '[email protected]' }; callback(null, userData); }, 1000); } // Użycie fetchUserData(123, (error, user) => { if (error) { console.error('Błąd:', error); return; } console.log('Użytkownik:', user); });

Problem: Callback Hell

Gdy musimy wykonać kilka operacji asynchronicznych jedna po drugiej, callbacks prowadzą do trudno czytelnego kodu zwanego "Callback Hell" lub "Pyramid of Doom":

fetchUser(userId, (userError, user) => { if (userError) { console.error(userError); return; } fetchUserPosts(user.id, (postsError, posts) => { if (postsError) { console.error(postsError); return; } fetchPostComments(posts[0].id, (commentsError, comments) => { if (commentsError) { console.error(commentsError); return; } // Wreszcie mamy wszystkie dane... console.log({ user, posts, comments }); }); }); });

Promise - rozwiązanie problemów callbacków

Promise to obiekt reprezentujący ewentualne zakończenie (lub niepowodzenie) operacji asynchronicznej. Promise może być w jednym z trzech stanów:

// Tworzenie Promise function fetchUserData(userId) { return new Promise((resolve, reject) => { setTimeout(() => { if (userId < 0) { reject(new Error('Nieprawidłowe ID użytkownika')); return; } const userData = { id: userId, name: 'Jan Kowalski', email: '[email protected]' }; resolve(userData); }, 1000); }); } // Użycie Promise z .then() i .catch() fetchUserData(123) .then(user => { console.log('Użytkownik:', user); return fetchUserPosts(user.id); }) .then(posts => { console.log('Posty:', posts); return fetchPostComments(posts[0].id); }) .then(comments => { console.log('Komentarze:', comments); }) .catch(error => { console.error('Wystąpił błąd:', error); });

Promise.all() i Promise.race()

JavaScript oferuje pomocnicze metody do pracy z wieloma Promise jednocześnie:

// Promise.all() - czeka na wszystkie Promise const userPromise = fetchUserData(123); const postsPromise = fetchUserPosts(123); const friendsPromise = fetchUserFriends(123); Promise.all([userPromise, postsPromise, friendsPromise]) .then(([user, posts, friends]) => { console.log('Wszystkie dane pobrane:', { user, posts, friends }); }) .catch(error => { console.error('Jeden z zapytań się nie powiódł:', error); }); // Promise.race() - zwraca pierwszy zakończony Promise Promise.race([ fetchFromServerA(), fetchFromServerB(), fetchFromServerC() ]) .then(result => { console.log('Najszybszy serwer odpowiedział:', result); }) .catch(error => { console.error('Błąd:', error); });

Async/Await - nowoczesna składnia

ES2017 wprowadził składnię async/await, która sprawia, że kod asynchroniczny wygląda i zachowuje się bardziej jak kod synchroniczny.

Kluczowe zasady async/await

  • async - funkcja zawsze zwraca Promise
  • await - można używać tylko wewnątrz funkcji async
  • await zatrzymuje wykonanie funkcji do rozwiązania Promise
// Przepisanie poprzedniego przykładu z async/await async function getUserData(userId) { try { const user = await fetchUserData(userId); console.log('Użytkownik:', user); const posts = await fetchUserPosts(user.id); console.log('Posty:', posts); const comments = await fetchPostComments(posts[0].id); console.log('Komentarze:', comments); return { user, posts, comments }; } catch (error) { console.error('Wystąpił błąd:', error); throw error; // Przekazujemy błąd dalej } } // Wywołanie getUserData(123) .then(data => console.log('Wszystkie dane:', data)) .catch(error => console.error('Główny błąd:', error));

Równoległe wykonywanie z async/await

Można łączyć async/await z Promise.all() dla równoległego wykonywania:

async function getUserFullData(userId) { try { // Te operacje wykonają się równolegle const [user, posts, friends] = await Promise.all([ fetchUserData(userId), fetchUserPosts(userId), fetchUserFriends(userId) ]); return { user, posts, friends }; } catch (error) { console.error('Błąd podczas pobierania danych:', error); throw error; } }

Obsługa błędów

Prawidłowa obsługa błędów w kodzie asynchronicznym jest kluczowa dla stabilności aplikacji.

// Szczegółowa obsługa błędów async function fetchWithRetry(url, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { console.warn(`Próba ${attempt} nieudana:`, error.message); if (attempt === maxRetries) { throw new Error(`Nie udało się pobrać danych po ${maxRetries} próbach`); } // Czekamy przed następną próbą (exponential backoff) await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000)); } } } // Użycie async function loadUserData() { try { const userData = await fetchWithRetry('/api/user/123'); console.log('Dane użytkownika:', userData); } catch (error) { console.error('Nie udało się załadować danych użytkownika:', error); // Pokazujemy użytkownikowi przyjazny komunikat showErrorMessage('Nie udało się załadować danych. Spróbuj ponownie później.'); } }

Praktyczne przykłady

Pobieranie danych z API

class ApiService { constructor(baseURL) { this.baseURL = baseURL; } async get(endpoint) { try { const response = await fetch(`${this.baseURL}${endpoint}`); if (!response.ok) { throw new Error(`API Error: ${response.status}`); } return await response.json(); } catch (error) { console.error('GET request failed:', error); throw error; } } async post(endpoint, data) { try { const response = await fetch(`${this.baseURL}${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data) }); if (!response.ok) { throw new Error(`API Error: ${response.status}`); } return await response.json(); } catch (error) { console.error('POST request failed:', error); throw error; } } } // Użycie const api = new ApiService('https://api.example.com'); async function createUser(userData) { try { const newUser = await api.post('/users', userData); console.log('Utworzono użytkownika:', newUser); return newUser; } catch (error) { console.error('Nie udało się utworzyć użytkownika:', error); throw error; } }

Oczekiwanie na wiele operacji

// Funkcja ładująca dashboard użytkownika async function loadUserDashboard(userId) { const loadingIndicator = showLoadingIndicator(); try { // Pobieramy wszystkie potrzebne dane równolegle const [ user, notifications, recentActivity, stats ] = await Promise.allSettled([ api.get(`/users/${userId}`), api.get(`/users/${userId}/notifications`), api.get(`/users/${userId}/activity`), api.get(`/users/${userId}/stats`) ]); // Sprawdzamy wyniki i obsługujemy częściowe błędy const dashboard = { user: user.status === 'fulfilled' ? user.value : null, notifications: notifications.status === 'fulfilled' ? notifications.value : [], recentActivity: recentActivity.status === 'fulfilled' ? recentActivity.value : [], stats: stats.status === 'fulfilled' ? stats.value : {} }; if (dashboard.user) { renderDashboard(dashboard); } else { throw new Error('Nie udało się załadować podstawowych danych użytkownika'); } } catch (error) { console.error('Błąd ładowania dashboardu:', error); showErrorMessage('Nie udało się załadować dashboardu'); } finally { hideLoadingIndicator(loadingIndicator); } }

Najlepsze praktyki

  • Używaj async/await dla lepszej czytelności kodu
  • Zawsze obsługuj błędy z try/catch
  • Wykorzystuj Promise.all() dla równoległych operacji
  • Unikaj mieszania Promise.then() z async/await
  • Używaj Promise.allSettled() gdy chcesz wykonać wszystkie operacje niezależnie od błędów

Asynchroniczność w JavaScript ewoluowała znacznie od czasów callbacków. Dzisiejsze narzędzia jak async/await sprawiają, że pisanie i utrzymywanie kodu asynchronicznego jest znacznie prostsze i przyjemniejsze. Opanowanie tych koncepcji jest kluczowe dla każdego współczesnego programisty JavaScript.

← Powrót do bloga