לדלג לתוכן

4.5 פרומיסים ו async await הרצאה

פרומיסים ו-async/await

ג׳אווהסקריפט היא שפה אסינכרונית - פעולות כמו קריאות רשת, קריאת קבצים וטיימרים לא חוסמות את הקוד. במקום לחכות שפעולה תסתיים, JS ממשיכה להריץ את הקוד הבא ומטפלת בתוצאה כשהיא מגיעה.

נלמד שלוש גישות לטיפול בקוד אסינכרוני: callbacks, Promises, ו-async/await.


קולבקים - callbacks

הגישה הישנה ביותר. פונקציה מקבלת פונקציה אחרת כפרמטר, וקוראת לה כשהפעולה מסתיימת:

function fetchData(url, callback) {
    // simulate async operation
    setTimeout(() => {
        const data = { name: "Alice", age: 25 };
        callback(null, data); // convention: first arg is error, second is result
    }, 1000);
}

fetchData("/api/user", (error, data) => {
    if (error) {
        console.error("Error:", error);
        return;
    }
    console.log("Data:", data);
});

console.log("This runs first!"); // runs before the callback

הבעיה - callback hell

כשיש כמה פעולות אסינכרוניות שתלויות זו בזו, הקוד הופך למפלצת של קינונים:

// callback hell / pyramid of doom
getUser(userId, (err, user) => {
    if (err) { handleError(err); return; }
    getOrders(user.id, (err, orders) => {
        if (err) { handleError(err); return; }
        getOrderDetails(orders[0].id, (err, details) => {
            if (err) { handleError(err); return; }
            getShippingStatus(details.shippingId, (err, status) => {
                if (err) { handleError(err); return; }
                console.log("Status:", status);
            });
        });
    });
});

זה קשה לקריאה, לתחזוקה ולטיפול בשגיאות. Promises פותרים את הבעיה הזו.


פרומיס - Promise

Promise הוא אובייקט שמייצג תוצאה עתידית - ערך שיהיה זמין בעתיד, או שגיאה.

לכל Promise יש שלושה מצבים:
- pending - ממתין (עדיין לא הסתיים)
- fulfilled - הצליח (יש תוצאה)
- rejected - נכשל (יש שגיאה)

יצירת Promise

const promise = new Promise((resolve, reject) => {
    // async operation
    setTimeout(() => {
        const success = true;

        if (success) {
            resolve("Data loaded!"); // fulfilled
        } else {
            reject(new Error("Failed to load")); // rejected
        }
    }, 1000);
});

שימוש עם then, catch, finally

promise
    .then((result) => {
        console.log("Success:", result); // "Data loaded!"
    })
    .catch((error) => {
        console.error("Error:", error.message);
    })
    .finally(() => {
        console.log("Done (success or failure)");
    });
  • then - רץ כש-Promise מצליח
  • catch - רץ כש-Promise נכשל
  • finally - רץ תמיד, בהצלחה ובכישלון

שרשור - chaining

כל then מחזיר Promise חדש, מה שמאפשר שרשור:

fetch("/api/user")
    .then((response) => {
        return response.json(); // returns a Promise
    })
    .then((user) => {
        console.log("User:", user.name);
        return fetch(`/api/orders/${user.id}`);
    })
    .then((response) => {
        return response.json();
    })
    .then((orders) => {
        console.log("Orders:", orders);
    })
    .catch((error) => {
        // catches errors from ANY step above
        console.error("Error:", error.message);
    });

השוו את זה ל-callback hell - הקוד שטוח וקריא הרבה יותר. ה-catch בסוף תופס שגיאות מכל השלבים.


מתודות סטטיות של Promise

Promise.all - כולם חייבים להצליח

מחכה שכל ה-Promises יסתיימו. אם אחד נכשל - הכל נכשל:

const promise1 = fetch("/api/users");
const promise2 = fetch("/api/posts");
const promise3 = fetch("/api/comments");

Promise.all([promise1, promise2, promise3])
    .then(([usersRes, postsRes, commentsRes]) => {
        // all three succeeded
        console.log("All loaded!");
    })
    .catch((error) => {
        // one of them failed
        console.error("One failed:", error);
    });

שימושי כשצריך כמה פעולות ביחד וכולן נדרשות.

Promise.allSettled - מחכה לכולם בלי קשר להצלחה

const promises = [
    fetch("/api/users"),
    fetch("/api/broken-endpoint"),
    fetch("/api/posts")
];

Promise.allSettled(promises)
    .then((results) => {
        results.forEach((result) => {
            if (result.status === "fulfilled") {
                console.log("Success:", result.value);
            } else {
                console.log("Failed:", result.reason);
            }
        });
    });

שימושי כשרוצים לדעת מה הצליח ומה נכשל, בלי שכישלון אחד יעצור את השאר.

Promise.race - הראשון שמסתיים

מחזיר את התוצאה של ה-Promise הראשון שמסתיים (הצלחה או כישלון):

// timeout pattern
function fetchWithTimeout(url, timeout) {
    const fetchPromise = fetch(url);
    const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error("Timeout!")), timeout);
    });

    return Promise.race([fetchPromise, timeoutPromise]);
}

fetchWithTimeout("/api/data", 5000)
    .then((response) => console.log("Got response"))
    .catch((error) => console.log(error.message)); // "Timeout!" if too slow

Promise.any - הראשון שמצליח

כמו race, אבל מתעלם מכישלונות - מחכה להצלחה הראשונה:

// try multiple sources, use whichever responds first
Promise.any([
    fetch("https://primary-api.com/data"),
    fetch("https://backup-api.com/data"),
    fetch("https://mirror-api.com/data")
])
    .then((response) => {
        console.log("Got response from fastest source");
    })
    .catch((error) => {
        // only if ALL failed
        console.error("All sources failed");
    });

סיכום מתודות סטטיות

מתודה מחכה ל... נכשל כש...
Promise.all כולם יצליחו אחד נכשל
Promise.allSettled כולם יסתיימו אף פעם לא נכשל
Promise.race הראשון שמסתיים הראשון שנכשל (אם הוא ראשון)
Promise.any הראשון שמצליח כולם נכשלים

async/await

תחביר מודרני שהופך קוד אסינכרוני להיראות כמו קוד סינכרוני. async/await בנוי מעל Promises - זה syntactic sugar.

// with Promises:
function getUser() {
    return fetch("/api/user")
        .then((response) => response.json())
        .then((user) => {
            console.log(user.name);
            return user;
        });
}

// with async/await - same thing, cleaner:
async function getUser() {
    const response = await fetch("/api/user");
    const user = await response.json();
    console.log(user.name);
    return user;
}
  • async לפני פונקציה - הפונקציה תמיד מחזירה Promise
  • await לפני ביטוי - מחכה ש-Promise יסתיים ומחזיר את הערך
  • await אפשר להשתמש רק בתוך פונקציה async (או ב-top level של מודול)

טיפול בשגיאות - try/catch

עם async/await, תופסים שגיאות עם try/catch רגיל:

async function fetchUserData(userId) {
    try {
        const response = await fetch(`/api/users/${userId}`);

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

        const user = await response.json();
        return user;
    } catch (error) {
        console.error("Failed to fetch user:", error.message);
        return null;
    } finally {
        console.log("Fetch attempt complete");
    }
}

const user = await fetchUserData(1);

אפשר גם try/catch ממוקד - לתפוס שגיאות רק ממקומות ספציפיים:

async function processData() {
    let response;
    try {
        response = await fetch("/api/data");
    } catch (error) {
        console.error("Network error:", error);
        return;
    }

    let data;
    try {
        data = await response.json();
    } catch (error) {
        console.error("Invalid JSON:", error);
        return;
    }

    // process data...
    console.log("Data:", data);
}

ריצה מקבילית מול ריצה סדרתית

סדרתי - sequential (איטי)

כל פעולה מחכה שהקודמת תסתיים:

async function sequential() {
    const user = await fetchUser(1);        // waits 1 second
    const posts = await fetchPosts(user.id); // then waits 1 second
    const comments = await fetchComments();  // then waits 1 second
    // total: ~3 seconds
}

זה הגיוני כשכל שלב תלוי בקודם (צריך את ה-user כדי לבקש את ה-posts שלו).

מקבילי - parallel (מהיר)

כשהפעולות עצמאיות, אפשר להריץ אותן במקביל:

async function parallel() {
    const [user, posts, comments] = await Promise.all([
        fetchUser(1),
        fetchPosts(),
        fetchComments()
    ]);
    // total: ~1 second (the slowest one)
}

כלל חשוב: אם הפעולות לא תלויות זו בזו, השתמשו ב-Promise.all להריצה מקבילית.

דפוס מעורב

לפעמים חלק מהפעולות תלויות וחלק לא:

async function mixed() {
    // step 1: get user (must be first)
    const user = await fetchUser(1);

    // step 2: get posts and friends in parallel (both depend on user, not on each other)
    const [posts, friends] = await Promise.all([
        fetchPosts(user.id),
        fetchFriends(user.id)
    ]);

    console.log(user, posts, friends);
}

דפוסים נפוצים

עיבוד מערך באופן סדרתי

// process items one by one (sequential)
async function processSequentially(urls) {
    const results = [];

    for (const url of urls) {
        const response = await fetch(url);
        const data = await response.json();
        results.push(data);
    }

    return results;
}

עיבוד מערך באופן מקבילי

// process all items at once (parallel)
async function processInParallel(urls) {
    const promises = urls.map(async (url) => {
        const response = await fetch(url);
        return response.json();
    });

    return Promise.all(promises);
}

retry - ניסיון חוזר

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}`);
            return await response.json();
        } catch (error) {
            console.log(`Attempt ${attempt} failed: ${error.message}`);
            if (attempt === maxRetries) throw error;

            // wait before retrying (exponential backoff)
            await new Promise(resolve =>
                setTimeout(resolve, 1000 * attempt)
            );
        }
    }
}

המרה מ-callback ל-Promise

// wrap a callback-based function in a Promise
function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

// usage
async function example() {
    console.log("Start");
    await delay(2000);
    console.log("2 seconds later");
}

async/await עם לולאות

שימו לב ש-await לא עובד כצפוי בתוך forEach:

// BUG: forEach doesn't wait for async callbacks
const urls = ["/api/1", "/api/2", "/api/3"];

urls.forEach(async (url) => {
    const response = await fetch(url);
    const data = await response.json();
    console.log(data); // these run in parallel, not sequential!
});
console.log("Done"); // this runs before the fetches complete!

הפתרון - השתמשו ב-for...of לריצה סדרתית:

// FIXED: for...of waits correctly
for (const url of urls) {
    const response = await fetch(url);
    const data = await response.json();
    console.log(data);
}
console.log("Done"); // runs after all fetches complete

או Promise.all עם map לריצה מקבילית:

// parallel with map + Promise.all
const results = await Promise.all(
    urls.map(async (url) => {
        const response = await fetch(url);
        return response.json();
    })
);
console.log("All done:", results);

השוואה לפייתון

פייתון תומך ב-async/await עם asyncio:

# import asyncio
#
# async def fetch_user(user_id):
#     await asyncio.sleep(1)  # simulate async work
#     return {"id": user_id, "name": "Alice"}
#
# async def main():
#     user = await fetch_user(1)
#     print(user)
#
# asyncio.run(main())
נושא פייתון ג׳אווהסקריפט
הגדרה async def func(): async function func() {}
המתנה await expr await expr
הרצה מקבילית asyncio.gather() Promise.all()
לולאת אירועים צריך asyncio.run() מובנית בסביבה
Promises asyncio.Future (פחות נפוץ) Promise (בסיס השפה)

ההבדל המרכזי: ב-JS כל דבר אסינכרוני בנוי סביב Promises ולולאת האירועים רצה תמיד. בפייתון, asyncio הוא תוספת שצריך להפעיל מפורשות. בפרקטיקה, קוד אסינכרוני הרבה יותר נפוץ ב-JS מאשר בפייתון.


סיכום

  • callbacks - הגישה הישנה, callback hell הוא הבעיה
  • Promises - אובייקט שמייצג תוצאה עתידית, then/catch/finally
  • שרשור - כל then מחזיר Promise חדש
  • Promise.all - מחכה לכולם (נכשל אם אחד נכשל)
  • Promise.allSettled - מחכה לכולם (מדווח על כל אחד)
  • Promise.race - הראשון שמסתיים
  • Promise.any - הראשון שמצליח
  • async/await - syntactic sugar מעל Promises, קוד נקי יותר
  • try/catch - טיפול בשגיאות עם async/await
  • ריצה מקבילית - השתמשו ב-Promise.all כשהפעולות עצמאיות
  • ריצה סדרתית - for...of עם await (לא forEach)
  • retry, delay, timeout - דפוסים נפוצים שחשוב להכיר