Kihagyás

Promise és async

A gyakorlat anyaga

Ezen a gyakorlaton megismerkedünk a JavaScript aszinkron programozásának egyik alapvető eszközével, a Promise-okkal.

Aszinkron programozás

A hagyományos, szinkron programozás során a kód sorról sorra, egymás után hajtódik végre. Minden utasítás megvárja az előző befejezését, mielőtt elkezdődne.

Példa - Szinkron kód:

1
2
3
console.log("First");
console.log("Second");
console.log("Third");

Kimenet:

1
2
3
First
Second
Third

Ez a megközelítés problémás lehet, ha olyan műveleteket végzünk, amelyek hosszú ideig tartanak (pl. hálózati kérés, fájl olvasása, adatbázis lekérdezés). Ezekben az esetekben a program "befagy", és nem tud mást csinálni, amíg a művelet be nem fejeződik.

Az aszinkron programozás lehetővé teszi, hogy egy hosszú művelet elindítása után a program továbbfusson, és csak akkor térjen vissza az eredményhez, amikor az készen van. A setTimeout egy beépített JavaScript függvény, amely egy megadott függvényt hajt végre egy bizonyos késleltetés (milliszekundumban megadva) után. Ez egy alapvető eszköz az aszinkron programozásban, amely lehetővé teszi kód végrehajtásának időzítését anélkül, hogy blokkolná a program további futását.

Példa - Aszinkron kód:

1
2
3
4
5
6
7
console.log("First");

setTimeout(() => {
    console.log("Second (after 1 second)");
}, 1000);

console.log("Third");

Kimenet:

1
2
3
First
Third
Second (after 1 second)

Látható, hogy a "Third" hamarabb íródik ki, mint a "Second", mert a setTimeout nem blokkolja a kód végrehajtását.

A modern webes alkalmazásokban gyakran találkozunk olyan műveletekkel, amelyek nem fejeződnek be azonnal:

  • Hálózati lekérés: API hívások, adatok letöltése szerverről
  • Fájlműveletek: Fájlok olvasása, írása (főleg szerveroldali programok esetén)
  • Adatbázis műveletek: Lekérés, tárolás, keresés (szerveroldali programok esetén)
  • Felhasználói interakciók: Gombok kattintása, input mezők változása
  • Időzítők: setTimeout, setInterval

A JavaScript egyetlen szálon (single-threaded módon) fut, így ha egy hosszú művelet blokkolná a szálat, az egész alkalmazás lefagyna, a felhasználói felület nem reagálna. Az aszinkron programozással elegánsan kezelhetjük ezeket a műveleteket anélkül, hogy a fő szálat blokkolnánk.

JavaScriptben az aszinkron programozás több fejlődési szakaszon ment keresztül:

  1. Callback függvények (korai megoldás) - nehezen olvasható, "callback hell" (lásd: előadás)
  2. Promise-ok (ES6, 2015) - strukturáltabb, láncolható
  3. Async/Await (ES2017) - legmodernebb, legolvashatóbb szintaxis

Ezen a gyakorlaton a Promise-okat és az async/await szintaxist fogjuk megismerni, amelyek a modern JavaScript aszinkron programozásának alapjai.

Promise

A Promise (ígéret) egy olyan objektum, amely egy aszinkron művelet végső sikerességét vagy sikertelenségét reprezentálja. Egy Promise három állapotban lehet:

  • pending (függőben): a művelet még nem fejeződött be
  • fulfilled (teljesült): a művelet sikeresen befejeződött
  • rejected (elutasított): a művelet hibával zárult

Egy Promise állapota csak egyszer változhat meg, és ezt követően már nem módosítható. Tehát amikor létrehozzunk pending lesz, és aztán vagy sikerül vagy nem (és ez végleges).

Promise létrehozása

A Promise-okat a Promise konstruktor segítségével hozhatjuk létre. A konstruktor egy függvényt vár paraméterül, amely két callback függvényt kap: resolve és reject (lehet csak resolve is).

Példa: Egy egyszerű Promise létrehozása

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const promise = new Promise((resolve, reject) => {
    // async operation
    const success = true;

    if (success) {
        resolve("Operation was successful!");
    } else {
        reject("An error occurred!");
    }
});

A resolve függvényt akkor hívjuk meg, ha a művelet sikeresen befejeződött. A reject függvényt akkor hívjuk meg, ha hiba történt.

Promise kezelése - then és catch

A Promise eredményét a then() és catch() metódusokkal kezelhetjük.

Példa: Promise eredményének kezelése

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const promise = new Promise((resolve, reject) => {
    const randomNumber = Math.random();

    if (randomNumber > 0.5) {
        resolve(`Success! Number: ${randomNumber}`);
    } else {
        reject(`Error! Number too small: ${randomNumber}`);
    }
});

promise
    .then(result => {
        console.log("Successful operation:", result);
    })
    .catch(error => {
        console.log("Error occurred:", error);
    });

A then() metódus akkor fut le, ha a Promise teljesült (resolved). A catch() metódus akkor fut le, ha a Promise elutasításra került (rejected).

Finally metódus

A finally() metódus mindig lefut, függetlenül attól, hogy a Promise teljesült vagy elutasításra került.

1
2
3
4
promise
    .then(result => console.log("Success:", result))
    .catch(error => console.log("Error:", error))
    .finally(() => console.log("Operation completed"));

Gyakorlati példa

Készítsünk egy függvényt, amely megadott idő után teljesül!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function wait(ms) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(`${ms} milliseconds have passed`);
        }, ms);
    });
}

wait(2000)
    .then(message => {
        console.log(message);
        return wait(1000);
    })
    .then(message => {
        console.log(message);
    });

Kimenet

1
2
2000 milliseconds have passed
1000 milliseconds have passed

Fetch API - Rövid bemutató

A fetch() a modern JavaScript beépített függvénye HTTP kérések küldésére. Promise-t ad vissza, amely a szerver válaszával teljesül. A példában egy vicceket tároló oldal REST API-ját fogjuk felhasználni, ami egy JSON objektumot ad vissza. Az oldal (ahol bővebb információ és végpontok is megtalálhatók): https://v2.jokeapi.dev/

1
2
3
4
fetch("https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,religious,political,racist,sexist,explicit")
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.log("Error:", error));

A fetch() csak hálózati hibánál dob hibát automatikusan! HTTP hibakódoknál (404, 500, stb.) a Promise sikeresen teljesül, de a válasz jelzi a hibát. Ezért érdemes ellenőrizni a response.ok értékét, ha a kérés nem volt sikeres!

1
2
3
4
5
6
7
8
9
fetch("https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,religious,political,racist,sexist,explicit")
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }
        return response.json();
    })
    .then(data => console.log(data))
    .catch(error => console.log("Error:", error.message));

Promise statikus metódusok

A Promise osztály számos hasznos statikus metódust biztosít több Promise kezeléséhez. Ezekből néhányat nézünk csak meg a gyakorlaton. Természetesen érdemes lehet minddel megismerkedni, azonban erre a gyakorlaton nem jut idő.

Promise.all()

A Promise.all() metódus egy tömbnyi Promise-t vár, és egy új Promise-t ad vissza, amely akkor teljesül, ha minden Promise teljesült. Ha bármelyik Promise elutasításra kerül, az egész Promise.all() is elutasításra kerül.

Példa: Három vicc párhuzamos lekérése

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
async function getThreeJokes() {
    try {
        console.log("Loading three jokes in parallel...\n");

        // Three fetch requests start in parallel
        const responses = await Promise.all([
            fetch("https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,religious,political,racist,sexist,explicit"),
            fetch("https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,religious,political,racist,sexist,explicit"),
            fetch("https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,religious,political,racist,sexist,explicit")
        ]);

        // Parse JSON for all responses
        const jokes = await Promise.all(
            responses.map(response => response.json())
        );

        // Display jokes
        jokes.forEach((jokeData, index) => {
            console.log(`\n--- Joke #${index + 1} ---`);
            if (jokeData.type === "single") {
                console.log(jokeData.joke);
            } else {
                console.log("Setup:", jokeData.setup);
                console.log("Delivery:", jokeData.delivery);
            }
        });

        return jokes;

    } catch (error) {
        console.log("Error:", error.message);
    }
}

getThreeJokes();

Kimenet példa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Loading three jokes in parallel...

--- Joke #1 ---
Setup: Why are modern programming languages so materialistic?
Delivery: Because they are object-oriented.

--- Joke #2 ---
UDP is better in the COVID era since it avoids unnecessary handshakes.

--- Joke #3 ---
Setup: How do you comfort a JavaScript bug?
Delivery: You console it.

Fontos: Ha bármelyik Promise hibát dob, az egész Promise.all() megszakad:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
async function demonstrateAllFailure() {
    try {
        const results = await Promise.all([
            fetch("https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,religious,political,racist,sexist,explicit"),
            fetch("https://invalid-url-that-fails.com/api"),  // This will fail
            fetch("https://v2.jokeapi.dev/joke/Christmas?blacklistFlags=nsfw,religious,political,racist,sexist,explicit")
        ]);
        console.log("All successful");
    } catch (error) {
        console.log("Promise.all failed:", error.message);
        // The third fetch won't even complete!
    }
}

Promise.any()

A Promise.any() az első sikeresen teljesült Promise eredményével tér vissza. Csak akkor utasításra kerül, ha minden Promise hibával zárul.

Példa: Több API endpoint közül az első sikeres használata

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
async function getFirstJoke() {
    try {
        // Request jokes from three different categories
        // Use the first successful result
        const response = await Promise.any([
            fetch("https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,religious,political,racist,sexist,explicit"),
            fetch("https://v2.jokeapi.dev/joke/Miscellaneous?blacklistFlags=nsfw,religious,political,racist,sexist,explicit"),
            fetch("https://v2.jokeapi.dev/joke/Christmas?blacklistFlags=nsfw,religious,political,racist,sexist,explicit")
        ]);

        const jokeData = await response.json();

        console.log(`First successful response (${jokeData.category} category):\n`);

        if (jokeData.type === "single") {
            console.log(jokeData.joke);
        } else {
            console.log("Setup:", jokeData.setup);
            console.log("Delivery:", jokeData.delivery);
        }

        return jokeData;

    } catch (error) {
        console.log("All requests failed:", error.message);
    }
}

getFirstJoke();

Kimenet

1
2
3
4
First successful response (Programming category):

Setup: Why do programmers always mix up Halloween and Christmas?
Delivery: Because Oct 31 equals Dec 25.

Async/Await

Bár a Promise-ok önmagukban is hatékonyak, a modern JavaScriptben gyakran az async/await szintaxist használjuk, amely olvashatóbbá teszi az aszinkron kódot.

Async függvények

Az async kulcsszóval deklarált függvények mindig Promise-t adnak vissza:

1
2
3
4
5
async function getData() {
    return 42;  // automatically becomes Promise.resolve(42)
}

getData().then(value => console.log(value));

Await kulcsszó

Az await kulcsszó megvárja egy Promise teljesülését, és visszaadja az eredményt. Csak async függvényeken belül használható!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
async function getRandomJoke() {
    try {
        const response = await fetch("https://v2.jokeapi.dev/joke/Any?blacklistFlags=nsfw,religious,political,racist,sexist,explicit");

        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }

        const jokeData = await response.json();

        // Two types of jokes: "single" or "twopart"
        if (jokeData.type === "single") {
            console.log("🤣 Joke:");
            console.log(jokeData.joke);
        } else if (jokeData.type === "twopart") {
            console.log("🤣 Joke:");
            console.log("Setup:", jokeData.setup);
            console.log("Delivery:", jokeData.delivery);
        }

        console.log(`\nCategory: ${jokeData.category}`);
        return jokeData;

    } catch (error) {
        console.log("Error fetching joke:", error.message);
        return null;
    }
}

getRandomJoke();

Kimenet

1
2
3
4
5
🤣 Joke:
Setup: Why are modern programming languages so materialistic?
Delivery: Because they are object-oriented.

Category: Programming

Több vicc lekérése egymás után:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
async function getTwoJokes() {
    console.log("Loading first joke...\n");
    await getRandomJoke();

    console.log("\n---\n");

    console.log("Loading second joke...\n");
    await getRandomJoke();
}

getTwoJokes();

Hibakezelés async/await-tel

Az async/await használatakor a hibakezelés try...catch blokkokkal történik:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
async function loadJoke() {
    try {
        const response = await fetch("https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,religious,political,racist,sexist,explicit");

        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }

        const data = await response.json();
        return data;
    } catch (error) {
        console.log("Error occurred:", error.message);
        return null;
    }
}

Párhuzamos műveletek async/await-tel

Ha több független aszinkron műveletet szeretnénk párhuzamosan futtatni, használjuk a Promise.all()-t:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
async function getJokesByCategory(categories) {
    try {
        console.log("Loading jokes from different categories...\n");

        // Fetch for each category
        const jokePromises = categories.map(category => 
            fetch(`https://v2.jokeapi.dev/joke/${category}?blacklistFlags=nsfw,religious,political,racist,sexist,explicit`)
                .then(response => response.json())
        );

        const jokes = await Promise.all(jokePromises);

        jokes.forEach((jokeData, index) => {
            console.log(`\n=== ${categories[index]} ===`);

            if (jokeData.error) {
                console.log("Error:", jokeData.message);
                return;
            }

            if (jokeData.type === "single") {
                console.log(jokeData.joke);
            } else {
                console.log(jokeData.setup);
                console.log("→", jokeData.delivery);
            }
        });

        return jokes;

    } catch (error) {
        console.log("Error fetching jokes:", error.message);
    }
}

// Usage - jokes from three different categories
getJokesByCategory(["Programming", "Miscellaneous", "Christmas"]);

Kimenet

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Loading jokes from different categories...

=== Programming ===
Setup: Why do Java developers wear glasses?
→ Because they can't C#.

=== Miscellaneous ===
I told my wife she was drawing her eyebrows too high. She looked surprised.

=== Dark ===
Setup: What's the difference between a hipster and a pizza?
→ A pizza can feed a family of four.

Gyakorlati példa - Komplett vicc alkalmazás (Service)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class JokeService {
    constructor() {
        this.baseUrl = "https://v2.jokeapi.dev/joke";
    }

    async getRandomJoke(category = "Any") {
        try {
            const response = await fetch(`${this.baseUrl}/${category}?blacklistFlags=nsfw,religious,political,racist,sexist,explicit`);

            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }

            return await response.json();
        } catch (error) {
            console.log("Error:", error.message);
            return null;
        }
    }

    async getMultipleJokes(count, category = "Programming") {
        try {
            // Fetch 'count' number of jokes in parallel
            const jokePromises = Array.from({ length: count }, () =>
                this.getRandomJoke(category)
            );

            const jokes = await Promise.all(jokePromises);
            return jokes.filter(joke => joke !== null);
        } catch (error) {
            console.log("Error fetching jokes:", error.message);
            return [];
        }
    }

    async getFirstAvailableJoke(categories) {
        try {
            // Get joke from the first available category
            const jokePromises = categories.map(category =>
                this.getRandomJoke(category)
            );

            return await Promise.any(jokePromises);
        } catch (error) {
            console.log("No categories available:", error.message);
            return null;
        }
    }

    displayJoke(jokeData) {
        if (!jokeData) return;

        console.log(`\n[${jokeData.category}]`);

        if (jokeData.type === "single") {
            console.log(jokeData.joke);
        } else {
            console.log(jokeData.setup);
            console.log("→", jokeData.delivery);
        }
    }
}

// Usage
const service = new JokeService();

// 1. One random joke
service.getRandomJoke("Programming")
    .then(joke => service.displayJoke(joke));

// 2. Five jokes in parallel
service.getMultipleJokes(5, "Programming")
    .then(jokes => {
        console.log(`\n=== ${jokes.length} jokes ===`);
        jokes.forEach(joke => service.displayJoke(joke));
    });

// 3. First available joke from multiple categories
service.getFirstAvailableJoke(["Programming", "Misc", "Pun"])
    .then(joke => {
        console.log("\n=== First available joke ===");
        service.displayJoke(joke);
    });