דילוג לתוכן
JavaScript

javascript promises ו־async/await ב־JavaScript: קוד אסינכרוני שאפשר להבין באמת

אם כתבתם JavaScript קצת מעבר ל־console.log, כנראה פגשתם את הביטוי javascript promises: קריאות לקובץ, בקשות API, טיימרים, פעולות בדפדפן, טעינת נתונים, וכל דבר שלא מסתיים מיד. הבעיה היא לא ש־Promises קשים במיוחד; הבעיה היא שקל לכתוב קוד אסינכרוני שנראה כאילו הוא עובד, עד שהוא נשבר במקום אחר לגמרי.

המטרה כאן היא מעשית: להבין מה Promise מייצג, איך משתמשים ב־then, catch ו־finally, מתי לעבור ל־async/await, ואיך להימנע מהטעויות הנפוצות שמייצרות הודעות שגיאה מבלבלות. זה לא מדריך תיאורטי על מנוע JavaScript, אלא מסלול עבודה שאפשר להריץ, לשנות ולבדוק.

אם אתם עדיין בתחילת הדרך עם סביבת פיתוח, כדאי להציץ גם במדריך צעדים ראשונים. ואם אתם מגיעים מרקע של פייתון, תמצאו כאן כמה הקבלות לכתיבה סינכרונית מול אסינכרונית, אבל הדוגמאות עצמן נשארות ב־JavaScript.

מה צריך להכין מראש

כדי להריץ את הדוגמאות במאמר, צריך התקנה עדכנית של Node.js. אין כאן ספריות חיצוניות, אין מפתחות API, ואין תלות בשירותים מרוחקים. כל הדוגמאות רצות מקומית.

  • פתחו תיקייה ריקה, למשל async-js-demo.
  • צרו קובץ בשם שמתאים לדוגמה, למשל promise-basic.js.
  • הריצו מהטרמינל: node promise-basic.js.
  • אם אתם עובדים בדפדפן, רוב העקרונות זהים, אבל הודעות שגיאה והיכולת להשתמש ב־await ברמה העליונה עשויות להשתנות.

מה זה Promise בפועל?

Promise הוא אובייקט שמייצג תוצאה עתידית. לא התוצאה עצמה, אלא הבטחה לכך שמתישהו נקבל אחת משתי אפשרויות: הצלחה או כישלון. ב־JavaScript קוראים למצבים האלה בדרך כלל:

  • pending — הפעולה עדיין לא הסתיימה.
  • fulfilled — הפעולה הסתיימה בהצלחה ויש ערך.
  • rejected — הפעולה נכשלה ויש שגיאה.

הנקודה החשובה: Promise לא עוצר את כל התוכנית עד שהתוצאה מוכנה. הוא מאפשר להתחיל פעולה, להמשיך הלאה, ולצרף קוד שירוץ כשהפעולה תסתיים.

דוגמה ראשונה: Promise בסיסי שאפשר להריץ

javascript
function wait(milliseconds) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(`Waited ${milliseconds} ms`);
    }, milliseconds);
  });
}

console.log("Before");

wait(500)
  .then((message) => {
    console.log(message);
  })
  .catch((error) => {
    console.error("Error:", error.message);
  })
  .finally(() => {
    console.log("Done");
  });

console.log("After");

שמרו את הקוד כ־promise-basic.js והריצו node promise-basic.js. הפלט הצפוי יהיה בערך:

text
Before
After
Waited 500 ms
Done

הסדר הזה חשוב. אם ציפיתם ש־After יודפס אחרון, זו בדיוק הנקודה ש־Promises באים להבהיר: הקריאה ל־wait(500) מתחילה פעולה אסינכרונית, אבל שאר הקוד ממשיך לרוץ. רק כשהטיימר מסתיים, הפונקציה שבתוך then מופעלת.

javascript promises: מתי משתמשים ב־then ומתי ב־catch?

ב־javascript promises, המתודה then משמשת לטיפול בהצלחה, ו־catch משמשת לטיפול בכישלון. finally רצה בשני המקרים, ולכן היא שימושית לניקוי מצב, סגירת חיווי טעינה, או הדפסת הודעה מסכמת.

מתודה מתי היא רצה שימוש נפוץ
then כאשר ה־Promise הצליח עיבוד הנתונים שהתקבלו
catch כאשר ה־Promise נכשל הצגת שגיאה או ניסיון התאוששות
finally אחרי הצלחה או כישלון ניקוי, לוג, איפוס מצב

דבר שמבלבל מתחילים: catch תופס גם שגיאה שנזרקה בתוך then. כלומר, אם פעולת ההמשך שלכם נכשלה, עדיין אפשר לטפל בזה בשרשרת אחת.

שרשור Promises בלי להיכנס לפירמידה

אחת הסיבות להשתמש ב־Promise היא היכולת לשרשר פעולות. אבל כדי שהשרשור יעבוד, חייבים להחזיר את ה־Promise הבא מתוך then. אם שוכחים return, השרשרת ממשיכה עם undefined, והתוצאה הופכת למבלבלת.

javascript
function getUserId() {
  return Promise.resolve(42);
}

function getUserName(userId) {
  return Promise.resolve(`User ${userId}`);
}

getUserId()
  .then((userId) => {
    console.log("User id:", userId);
    return getUserName(userId);
  })
  .then((userName) => {
    console.log("User name:", userName);
  })
  .catch((error) => {
    console.error("Failed:", error.message);
  });

שמרו כ־promise-chain.js והריצו. הפלט הצפוי:

text
User id: 42
User name: User 42

בדוגמה הזאת אין באמת מסד נתונים או API, אבל המבנה זהה לקוד אמיתי: קודם מקבלים מזהה, אחר כך משתמשים בו כדי להביא נתון נוסף. היתרון של השרשור הוא שכל שלב מקבל את הערך מהשלב הקודם, וכל שגיאה בדרך מגיעה ל־catch המרכזי.

למה async/await מרגיש טבעי יותר

async/await הוא תחביר שנבנה מעל Promises. הוא לא מחליף אותם באמת; הוא פשוט מאפשר לכתוב קוד אסינכרוני בצורה שנראית יותר כמו קוד רגיל. פונקציה שמוגדרת עם async תמיד מחזירה Promise, גם אם החזרתם ממנה ערך פשוט.

המילה await אומרת: בתוך הפונקציה האסינכרונית הזאת, המתן עד שה־Promise יסתיים, ואז החזר לי את הערך שלו. בזמן הזה JavaScript לא מקפיא את כל התוכנית; הוא רק ממשיך את הפונקציה הזאת מאוחר יותר.

דוגמה שלישית: אותו רעיון עם async/await

javascript
function readSetting(name) {
  const settings = {
    theme: "dark",
    language: "he"
  };

  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Object.hasOwn(settings, name)) {
        resolve(settings[name]);
      } else {
        reject(new Error(`Missing setting: ${name}`));
      }
    }, 300);
  });
}

async function main() {
  try {
    const theme = await readSetting("theme");
    const language = await readSetting("language");

    console.log("Theme:", theme);
    console.log("Language:", language);
  } catch (error) {
    console.error("Could not read settings:", error.message);
  } finally {
    console.log("Finished reading settings");
  }
}

main();

שמרו כ־async-await-settings.js והריצו. הפלט הצפוי:

text
Theme: dark
Language: he
Finished reading settings

כאן הקוד קל יותר לקריאה: קודם קוראים את theme, אחר כך את language, ואז מדפיסים. הטיפול בשגיאות נעשה עם try/catch, כמו בקוד סינכרוני. אם תשנו את אחת הקריאות ל־readSetting("timezone"), תגיעו ל־catch ותראו הודעת שגיאה מסודרת.

הרצה במקביל עם Promise.all

לא כל פעולות ה־await צריכות לרוץ אחת אחרי השנייה. אם שתי פעולות לא תלויות זו בזו, אפשר להריץ אותן במקביל עם Promise.all. זה שימושי במיוחד כשמביאים כמה נתונים עצמאיים: פרופיל משתמש והעדפות, רשימת מוצרים וסיכום סל, או כמה קבצים מקומיים.

javascript
function task(name, milliseconds) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(`${name} finished`);
    }, milliseconds);
  });
}

async function main() {
  const startedAt = Date.now();

  const results = await Promise.all([
    task("First task", 600),
    task("Second task", 400),
    task("Third task", 200)
  ]);

  console.log(results);
  console.log("Total time:", Date.now() - startedAt, "ms");
}

main();

שמרו כ־promise-all.js והריצו. הפלט הצפוי ייראה כך:

text
[ 'First task finished', 'Second task finished', 'Third task finished' ]
Total time: 600 ms

המספר המדויק יכול להשתנות מעט, אבל הוא אמור להיות קרוב לזמן של המשימה הארוכה ביותר, לא לסכום כל המשימות. כלומר, לא בערך 1200 מילישניות, אלא בערך 600. זו אחת ההבנות החשובות בקוד אסינכרוני: לפעמים הפתרון הנכון הוא לא “לחכות פחות”, אלא “להתחיל מוקדם יותר”.

מתי לא להשתמש ב־Promise.all

Promise.all נכשל מיד כאשר אחד ה־Promises נכשל. זה מצוין כשכל התוצאות הכרחיות, אבל פחות מתאים אם אפשר להמשיך גם עם חלק מהנתונים. במקרים כאלה אפשר להשתמש ב־Promise.allSettled, שמחזיר סטטוס לכל פעולה.

לדוגמה, אם אתם טוענים שלוש הגדרות אופציונליות, לא תמיד הגיוני להפיל את כל המסך בגלל אחת חסרה. לעומת זאת, אם אתם שומרים הזמנה וחייבים שגם התשלום וגם המלאי יתעדכנו, כישלון של שלב אחד צריך לעצור את התהליך. ההחלטה כאן היא לוגית, לא רק טכנית.

הבדל קצר בין קוד סינכרוני לאסינכרוני

בקוד סינכרוני, כל שורה מחכה לשורה שלפניה. זה נוח להבנה, אבל לא מתאים לפעולות שלוקחות זמן. בקוד אסינכרוני, מתחילים פעולה ומגדירים מה יקרה כשהיא תסתיים. async/await נותן תחושה של קוד סינכרוני, אבל שומר על היתרון של אסינכרוניות.

מי שמגיע מ־פייתון עשוי לזהות דמיון רעיוני ל־async ו־await גם שם, אבל חשוב לא להניח שהכול זהה. לכל שפה יש מודל הרצה וספריות משלה. ב־JavaScript, Promises הם חלק מרכזי מהשפה ומהסביבה, במיוחד בדפדפן וב־Node.js.

טעויות נפוצות והודעות שגיאה אמיתיות

שימוש ב־await מחוץ לפונקציה async

קוד כזה עלול להיכשל, במיוחד אם הקובץ רץ כ־CommonJS ב־Node.js:

javascript
const value = await Promise.resolve("hello");
console.log(value);

הודעת שגיאה נפוצה:

output
SyntaxError: await is only valid in async functions and the top level bodies of modules

הפתרון הפשוט הוא לעטוף את הקוד בפונקציה async ולהפעיל אותה:

javascript
async function main() {
  const value = await Promise.resolve("hello");
  console.log(value);
}

main();

הפלט:

text
hello

שוכחים להחזיר Promise מתוך פונקציה

טעות קלאסית היא ליצור Promise בתוך פונקציה, אבל לא להחזיר אותו:

javascript
function loadName() {
  Promise.resolve("Dana");
}

loadName().then((name) => {
  console.log(name);
});

הודעת שגיאה נפוצה:

output
TypeError: Cannot read properties of undefined (reading 'then')

הבעיה היא ש־loadName() מחזירה undefined. התיקון:

javascript
function loadName() {
  return Promise.resolve("Dana");
}

loadName().then((name) => {
  console.log(name);
});

הפלט:

text
Dana

לא מטפלים בדחייה של Promise

אם Promise נדחה ואין catch או try/catch, תראו בדפדפן לעיתים הודעה כמו:

text
Uncaught (in promise) Error: Something failed

ב־Node.js תקבלו בדרך כלל הדפסה של השגיאה והמחסנית שלה. הפתרון הוא לא להתעלם מכישלונות:

javascript
async function main() {
  try {
    await Promise.reject(new Error("Something failed"));
  } catch (error) {
    console.error("Handled:", error.message);
  }
}

main();

הפלט:

text
Handled: Something failed

איך לחשוב על זרימת קוד אסינכרונית

כשאתם כותבים פונקציה אסינכרונית, שאלו שלוש שאלות פשוטות:

  1. מה תלוי במה? אם פעולה ב׳ צריכה את התוצאה של פעולה א׳, השתמשו ב־await לפי הסדר.
  2. מה יכול לרוץ במקביל? אם פעולות לא תלויות זו בזו, שקלו Promise.all.
  3. מה קורה בכישלון? אל תשאירו Promise בלי טיפול. החליטו אם להציג שגיאה, להחזיר ערך ברירת מחדל, או לעצור את הפעולה.

הרבה באגים בקוד אסינכרוני הם לא באגים של תחביר, אלא באגים של הנחות: חשבתם שמשתנה כבר התמלא, אבל הוא עדיין בדרך; חשבתם ששתי פעולות רצות אחת אחרי השנייה, אבל הן רצות במקביל; או להפך, חיכיתם לכל דבר בנפרד למרות שאפשר היה להתחיל הכול יחד.

דפוס עבודה מומלץ לקוד נקי

בפרויקטים קטנים וגדולים כאחד, כדאי לאמץ כמה הרגלים פשוטים:

  • העדיפו async/await לקוד עסקי — הוא בדרך כלל קריא יותר משרשראות ארוכות של then.
  • השאירו then למקרים קצרים — למשל בדיקות קטנות או שרשרת פשוטה מאוד.
  • אל תבלעו שגיאות בשקט — catch ריק מקשה מאוד על איתור תקלות.
  • תנו שמות ברורים לפונקציות אסינכרוניות — למשל loadSettings, saveUser, readFileContent.
  • בדקו את מסלול הכישלון — לא רק את המקרה שבו הכול מצליח.

אם אתם בונים לעצמכם בסיס JavaScript רחב יותר, אפשר לשלב את הנושא הזה עם תרגול של פקודות טרמינל, Git, ובדיקות קטנות. מדריך צעדים ראשונים יכול לעזור לסדר את סביבת העבודה לפני שמעמיקים בכלים נוספים.

שאלות שקוראים באמת שואלים

האם async/await מחליף את Promises?

לא. async/await משתמש ב־Promises מאחורי הקלעים. פונקציה עם async מחזירה Promise, ו־await מחכה ל־Promise. לכן חשוב להבין לפחות את הבסיס של then ו־catch.

האם חייבים לכתוב catch לכל Promise?

צריך שיהיה טיפול בשגיאה במקום כלשהו בשרשרת. זה יכול להיות catch בסוף שרשרת Promises, או try/catch סביב await. מה שלא כדאי לעשות הוא להשאיר כישלונות בלי שום טיפול.

למה הפונקציה שלי מחזירה Promise במקום ערך?

כי היא הוגדרה עם async, או כי היא מחזירה פעולה אסינכרונית. כדי לקבל את הערך, צריך להשתמש ב־await בתוך פונקציה אסינכרונית, או ב־then.

סיכום: פחות קסם, יותר סדר

Promises ו־async/await לא נועדו להפוך JavaScript למסובך יותר. הם נועדו לתת דרך מסודרת לטפל בפעולות שלוקחות זמן: להמתין לתוצאה, לטפל בכישלון, להריץ פעולות במקביל כשאפשר, ולכתוב קוד שאפשר לקרוא גם חודש אחרי שנכתב.

הדרך הטובה להשתפר היא לא לשנן הגדרות, אלא להריץ דוגמאות קטנות: Promise שמצליח, Promise שנכשל, שרשור של שתי פעולות, async/await עם try/catch, ואז Promise.all. אחרי כמה תרגולים כאלה, קוד אסינכרוני מפסיק להיראות כמו טריק ומתחיל להרגיש כמו כלי עבודה רגיל.