Co to jest async/await w JavaScript?

JavaScript

readTime

5 min

Co to jest async/await w JavaScript?

Jeśli dopiero zaczynasz przygodę z programowaniem, zwłaszcza w języku JavaScript, na pewno spotkałeś się z terminami jak "asynchroniczność" czy "Promisy".

Kiedy programujesz, w JavaScript wiele operacji jest asynchronicznych — szczególnie tych związanych z wykonywaniem różnych zapytań w Internecie. Przykładowo takich jak pobieranie danych z API.

Tutaj wkracza do gry async/await, czyli słowa kluczowe, które pojawiły się w standardzie ECMAScript i zrewolucjonizowały pisanie asynchronicznego kodu.

Dzięki async i await pisanie kodu, który zarządza operacjami asynchronicznymi, jest o wiele bardziej czytelne i łatwiejsze w utrzymaniu.

Co to jest async/await?

Async/await to po prostu sposób na pisanie asynchronicznego kodu w stylu synchronicznym.

Czyli tak naprawdę będzie to kod asynchroniczny ale wizualnie wydaje się nam jakbyśmy pisali normalny kod synchroniczny. Czyli taki który będzie wykonywany linijka po linijce.

Zamiast używać callbacków, Promisów i zagnieżdżać je w kodzie, możesz oznaczyć funkcję jako async i używać await, aby "poczekać" na wynik operacji asynchronicznej, zanim przejdziesz dalej.

Historia i rozwój async/await

Async/await zostało dodane do języka JavaScript w 2017 roku w standardzie ECMAScript 2017 (ES8).

Jest to odpowiedź na zawiłości i problemy wynikające z wcześniejszych metod obsługi asynchroniczności, jak callbacki i Promisy.

Callbacki prowadziły do tzw. "callback hell", czyli głęboko zagnieżdżonych wywołań, które stawały się trudne do zarządzania.

Potem przyszły Promisy, które były lepsze, ale nie rozwiązywały wszystkich problemów. Dopiero async/await umożliwiło pisanie czytelniejszego i bardziej intuicyjnego kodu.

Porównanie z innymi metodami asynchronicznymi

Callbacki były pierwszą metodą obsługi asynchroniczności w JavaScript. Problem polegał na tym, że kiedy trzeba było wywołać kolejne operacje asynchroniczne, kod szybko stawał się trudny do zarządzania.

Promisy były dużym krokiem naprzód, pozwalając na chaining za pomocą metody .then(), co zmniejszało zagnieżdżenie, ale wciąż było to mniej intuicyjne niż kod synchroniczny.

Async/await zrewolucjonizowało pisanie kodu asynchronicznego, ponieważ pozwala na używanie Promisów w sposób bardziej przypominający kod synchroniczny.

Dzięki async/await możemy uniknąć zagnieżdżania, a nasz kod jest bardziej przejrzysty.

Jak działa async/await w JavaScript?

Funkcja oznaczona słowem kluczowym async zawsze zwraca Promise.

Natomiast await można używać wewnątrz funkcji async, aby poczekać na zakończenie operacji asynchronicznej. Await zatrzymuje wykonanie funkcji, aż Promise zostanie rozwiązany (resolve) lub odrzucony (reject).

Przykład:

javascript
async function downloadData() {
  const response = await fetch("https://api.example.com/dane");
  const data = await response.json();
  return data;
}

Jak widzisz, funkcja downloadData używa await, aby poczekać na odpowiedź z API. Dzięki temu kod wygląda jak synchroniczny, mimo że jest asynchroniczny.

Dlaczego warto używać async/await?

  • Czytelność: Kod napisany z użyciem async/await jest łatwiejszy do zrozumienia, zwłaszcza dla osób początkujących.
  • Obsługa błędów: Zamiast używać .catch() na Promisach, możesz użyć try/catch wewnątrz funkcji async, co jest bardziej intuicyjne.

Promisy jako podstawa async/await

Żeby zrozumieć async/await, najpierw trzeba zrozumieć Promisy. Async/await to po prostu lepsza składnia dla Promisów.

Co to jest Promise?

Promise to obiekt, który reprezentuje operację asynchroniczną i zwraca obietnicę (promise), że operacja ta zakończy się w przyszłości.

Może zakończyć się sukcesem (fulfilled) lub porażką (rejected).

Jak działają Promisy w JavaScript?

Kiedy tworzysz nowy Promise, dajesz JavaScriptowi instrukcję do wykonania operacji asynchronicznej.

Jeśli operacja się uda, wywołujesz funkcję resolve, jeśli się nie uda — funkcję reject.

javascript
let myPromise = new Promise((resolve, reject) => {
  setTimeout(() => resolve("Sukces!"), 1000);
});

Tutaj mamy prosty Promise, który po 1 sekundzie zwróci wartość "Sukces!". Możesz na nim wykonać operacje za pomocą .then().

javascript
let myPromise = new Promise((resolve, reject) => {
  setTimeout(() => resolve("Sukces"), 1000);
});

myPromise.then((results) => {
  console.log(results);
});

Stany Promisów: pending, fulfilled, rejected

  • Pending: Operacja się wykonuje, ale nie wiadomo jeszcze, czy zakończy się sukcesem, czy porażką.
  • Fulfilled: Operacja zakończyła się sukcesem i Promise został rozwiązany.
  • Rejected: Operacja zakończyła się porażką i Promise został odrzucony.

Tworzenie i użycie Promisów

Aby stworzyć Promise, używasz konstrukcji new Promise((resolve, reject) => {...}). Możesz go później użyć, wywołując .then().

javascript
myPromise.then((value) => {
  console.log(value); // 'Sukces!'
});

Składnia async/await

Kiedy oznaczasz funkcję słowem kluczowym async, funkcja ta zawsze zwraca Promise. await czeka na zakończenie Promisa.

Definicja funkcji asynchronicznej przy użyciu async

javascript
async function asyncFunc() {
  return "Wynik";
}

W powyższym przykładzie asyncFunc zwraca Promis. Możemy go wywołać w dowolnym miejscu w kodzie.

Użycie await w funkcjach async

await używasz w funkcji async, aby poczekać na zakończenie operacji asynchronicznej.

javascript
async function downloadData() {
  const dane = await fetch("https://api.example.com");
  return dane.json();
}

Obsługa błędów za pomocą try/catch

Obsługa błędów w async/await działa z użyciem try/catch.

javascript
async function downloadData() {
  try {
    const data = await fetch("https://api.example.com");
    return data.json();
  } catch (error) {
    console.log("Błąd:", error);
  }
}

Ograniczenia i błędy składniowe

Musisz pamiętać, że await może być użyte tylko wewnątrz funkcji oznaczonej jako async.

Zastosowania async/await

Async/await znajduje swoje zastosowanie w różnych scenariuszach.

Przeanalizujmy kilka z nich:

Asynchroniczne pobieranie danych z API

Jednym z najbardziej powszechnych zastosowań async/await jest pobieranie danych z API.

Dzięki tej konstrukcji możesz w prosty sposób poczekać na odpowiedź z serwera i przetworzyć dane bez zagnieżdżania wywołań callbacków czy chainingu Promisów.

javascript
async function downloadData() {
  try {
    const response = await fetch("https://api.example.com/dane");
    const dane = await response.json();
    return dane;
  } catch (error) {
    console.error("Błąd pobierania danych:", error);
  }
}

W tym przykładzie, dzięki async i await, kod jest prosty i czytelny, a obsługa błędów za pomocą try/catch pozwala na łatwe zarządzanie ewentualnymi problemami.

Przetwarzanie danych i operacje na plikach

Operacje na plikach często wymagają asynchroniczności, zwłaszcza w środowiskach sieciowych. Możesz użyć async/await, aby przetworzyć pliki, poczekać na zakończenie ich przetwarzania, a następnie wykonać kolejne operacje.

javascript
async function transformData(file) {
  try {
    const data = await file.read();
    console.log("Dane z pliku:", data);
  } catch (error) {
    console.log("Błąd przetwarzania pliku:", error);
  }
}

Integracja z bazami danych

W przypadku aplikacji, które wymagają integracji z bazami danych, async/await ułatwia zarządzanie zapytaniami i wynikami asynchronicznych operacji na bazach danych, takich jak MongoDB, MySQL, czy PostgreSQL.

javascript
async function downloadUser(id) {
  try {
    const user = await db.query(`SELECT * FROM users WHERE id = ${id}`);
    return user;
  } catch (error) {
    console.error("Błąd przy pobieraniu użytkownika:", error);
  }
}

Zarządzanie operacjami sieciowymi

Jeśli Twoja aplikacja wymaga wielu operacji sieciowych, async/await pomaga zorganizować te wywołania w sposób bardziej przejrzysty i łatwiejszy do debugowania.

Na przykład, w aplikacjach webowych, które pobierają dane z różnych źródeł, async/await ułatwia przetwarzanie wyników.


Najlepsze praktyki i wzorce projektowe

Chociaż async/await jest bardzo użyteczne, warto przestrzegać kilku dobrych praktyk, aby Twój kod był optymalny i łatwy w utrzymaniu.

Unikanie zagnieżdżania Promisów

Jednym z głównych celów async/await było wyeliminowanie zjawiska zagnieżdżania kodu asynchronicznego, czyli tzw. "callback hell".

Korzystając z async/await, nie musisz łączyć Promisów za pomocą .then(), ponieważ możesz po prostu "czekać" na wynik w sposób synchroniczny.

Porządkowanie kodu asynchronicznego

Stosowanie async/await pozwala na pisanie bardziej czytelnego i zorganizowanego kodu.

Każda funkcja asynchroniczna powinna być wyraźnie oznaczona słowem kluczowym async, co ułatwia śledzenie, które operacje są wykonywane asynchronicznie.

Użycie async/await w modułach ES6

Async/await działa doskonale w połączeniu z modułami ES6.

Możesz z łatwością importować funkcje asynchroniczne między modułami, co pozwala na lepszą organizację i modularność kodu.

javascript
import { donwloadData } from "./data";

async function showData() {
  const data = await downloadData();
  console.log(data);
}

Wzorce projektowe: tryb periodyczny, kolejki zadań

W niektórych przypadkach, takich jak przetwarzanie danych w trybie periodycznym (np. cykliczne zadania wykonywane na serwerze), async/await jest idealnym rozwiązaniem.

Możesz również użyć async/await do zarządzania kolejkami zadań, co pozwala na uporządkowanie zadań do wykonania w odpowiedniej kolejności.

Optymalizacja wydajności przy użyciu async/await

Async/await pozwala na wykonywanie wielu operacji asynchronicznych równocześnie, co jest korzystne dla wydajności aplikacji.

Możesz na przykład pobierać dane z kilku źródeł jednocześnie, zamiast czekać na zakończenie jednej operacji przed rozpoczęciem kolejnej.

javascript
async function multiDownload() {
  const [data1, data2] = await Promise.all([
    fetch("https://api.example.com/dane1"),
    fetch("https://api.example.com/dane2"),
  ]);
  console.log(data1, data2);
}

Debugowanie kodu z async/await

Pisząc kod asynchroniczny, niezależnie od tego, jak dobrze go napiszesz, mogą pojawić się błędy. Dlatego istotne jest posiadanie narzędzi i metod do debugowania.

Śledzenie błędów w kodzie asynchronicznym

Błędy w kodzie asynchronicznym mogą być trudne do wychwycenia.

Dlatego stosowanie try/catch wewnątrz funkcji async jest najlepszą praktyką, która pozwala na lepszą kontrolę nad ewentualnymi problemami.

Rozwiązywanie typowych problemów z async/await

Niektóre problemy związane z async/await mogą wynikać z błędnego zarządzania Promisami.

Na przykład, jeśli nie zastosujesz await w odpowiednim miejscu, funkcja może zwrócić Promise zanim zakończy się operacja, co może prowadzić do błędów.


Podsumowanie

Async/await ułatwia zarządzanie asynchronicznością w języku JavaScript.

Z jego pomocą możesz pisać bardziej czytelny i lepiej zorganizowany kod, unikając problemów wynikających z używania callbacków lub Promisów w ich tradycyjnej formie.

Async/await to szczególnie dobre rozwiązanie, jeśli chodzi o operacje sieciowe, przetwarzanie danych czy pracę z bazami danych.

authorImg

Witek Pruchnicki

Z pasją dzielę się wiedzą o programowaniu i nie tylko na różne sposoby