Menu

Promise ב-JavaScript: then, catch ו-Promise.all בהסבר פשוט

איך Promises עובדים ב-JavaScript: שלושת המצבים, שרשור עם then ו-catch, שילוב עם Promise.all וכתיבת Promise משלכם עם new Promise.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

Promise הוא ממלא מקום לערך עתידי

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

ה-console.log הראשון מציג Promise במצב pending. חצי שנייה אחר כך ה-Promise מתממש, וה-callback של .then רץ עם הערך. ה-Promise עצמו הוא סתם אובייקט; הקסם הוא שהוא יודע להודיע לכל מי שמאזין כשהערך שלו מגיע.

שלושת המצבים

Promise תמיד נמצא באחד משלושה מצבים:

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

Promise עובר מ-pending ל-fulfilled או ל-rejected פעם אחת בדיוק, ונשאר שם לתמיד. אי אפשר לבטל מימוש של Promise או לממש אותו פעמיים.

Promise.resolve(value) בונה Promise שכבר התממש; Promise.reject(error) בונה Promise שכבר נדחה. זה נוח לבדיקות ולהחזרת Promise מפונקציה שלפעמים יש לה את התשובה מיד.

קריאת הערך: .then ו-.catch

לא פותחים Promise ישירות: מעבירים callback ל-.then, וה-Promise קורא לו כשהערך מוכן:

.catch(fn) רץ אם ה-Promise נדחה. מאחורי הקלעים זה קיצור של .then(undefined, fn). .catch() בסוף השרשרת מטפל בדחיות מ_כל_ שלב שמעליו, ולא צריך אחד כזה אחרי כל .then.

שרשור: כל .then מחזיר Promise חדש

זה החלק שמבלבל אנשים. .then() לא רק מריץ callback: הוא מחזיר Promise חדש שמתממש עם מה שה-callback החזיר. זה מה שמאפשר שרשור:

כל שלב מזין את הבא. אם callback של .then מחזיר Promise, השרשרת מחכה ל_אותו_ Promise לפני שהיא ממשיכה, כך ששלבים אסינכרוניים מתחברים בצורה נקייה:

שלושה שלבים אסינכרוניים ברצף, בלי קינון. השוו את זה לאותה לוגיקה שנכתבה עם callbacks ותבינו למה Promises תפסו.

שגיאות נופלות לאורך השרשרת

Promise שנדחה מדלג על כל .then עד שהוא מוצא .catch. זה כל מודל הטיפול בשגיאות:

זריקת שגיאה בתוך .then דוחה את ה-Promise שאותו .then החזיר. ה-.then הבא רואה את הדחייה ומעביר אותה הלאה, עד ש-.catch בולע אותה. בדרך כלל .catch אחד בסוף השרשרת זה כל מה שצריך, ושרשרת בלי .catch בכלל תייצר אזהרת "unhandled promise rejection", שכדאי לתקן.

בניית Promise משלכם עם new Promise

ברוב המקרים תשתמשו ב-Promises שספריות נותנות לכם. לפעמים צריך לעטוף משהו שלא מחזיר Promise, בדרך כלל API ישן מבוסס callbacks:

הפונקציה שמעבירים ל-new Promise נקראת executor. היא מקבלת שני ארגומנטים: resolve (קוראים לו עם ערך ההצלחה) ו-reject (קוראים לו עם שגיאה). קראו לאחד מהם פעם אחת בדיוק. אחרי זה, קריאות נוספות מתעלמות מהן.

שני הרגלים שחוסכים כאב ראש:

  • השתמשו ב-new Promise רק כשעוטפים משהו שעוד לא מבוסס על Promises. אם פונקציה כבר מחזירה Promise, פשוט החזירו אותו.
  • תמיד בצעו reject עם אובייקט Error, לא עם מחרוזת. שווה לשמור את ה-stack trace.

הרצת דברים במקביל: Promise.all

שרשראות .then רצות ברצף. כשיש כמה משימות אסינכרוניות בלתי תלויות ורוצים שירוצו באותו זמן, Promise.all הוא הכלי:

שלושת הטיימרים רצים במקביל. Promise.all מתממש עם מערך של תוצאות באותו סדר כמו הקלט, אחרי שכל ה-Promises התממשו. הזמן הכולל הוא בערך 400ms, לא 900ms.

המלכודת: Promise.all נדחה ברגע ש_אחד כלשהו_ מה-Promises שלו נדחה, והתוצאות האחרות הולכות לאיבוד. זו ההתנהגות הנכונה כשצריך את כל החלקים (למשל, הצגת דף שדורש שלוש קריאות API). כשלא, השתמשו ב-allSettled.

כשחלק מהכשלונות בסדר: Promise.allSettled

Promise.allSettled מחכה שכל Promise יסתיים, בהצלחה או בדחייה, ונותן לכם דוח:

כל תוצאה היא אובייקט: { status: "fulfilled", value } או { status: "rejected", reason }. זה שימושי כשהצלחה חלקית מקובלת: רישום קבוצת אירועים בלוג, טעינה של הרבה תמונות ממוזערות, הרצת בדיקות תקינות עצמאיות.

עוד שני כלים ששווה להכיר:

  • Promise.race([...]): מסתיים ברגע שה-Promise הראשון מסתיים, בהצלחה או בכישלון. נוח ל-timeouts.
  • Promise.any([...]): מתממש עם ההצלחה הראשונה ומתעלם מדחיות. נדחה רק אם כל ה-Promises נדחים.

Promises תמיד אסינכרוניים

גם Promise שכבר התממש קורא ל-callback של .then שלו באופן אסינכרוני: אף פעם לא באופן סינכרוני, אף פעם לא באותו tick:

הפלט הוא לפני, אחרי, מיידי. ה-callback של .then מחכה שהקוד הנוכחי יסתיים, ואז רץ בתור ה-microtasks. הכלל הזה, "callback של Promise אף פעם לא רץ באופן סינכרוני", הוא הסיבה ששילוב Promises עם קוד סינכרוני צפוי: הקוד הסינכרוני תמיד מסתיים קודם.

הבא בתור: async/await

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

שאלות נפוצות

מה זה Promise ב-JavaScript?

Promise הוא אובייקט שמייצג ערך שעדיין לא מוכן, בדרך כלל התוצאה העתידית של פעולה אסינכרונית כמו בקשת רשת. הוא תמיד נמצא באחד משלושה מצבים: pending, fulfilled או rejected. קוראים את הערך הסופי על ידי חיבור פונקציות callback עם .then() ו-.catch().

מה ההבדל בין then ל-catch?

.then(onFulfilled) רץ כשה-Promise מסתיים בהצלחה ומקבל את הערך שהתקבל. .catch(onRejected) רץ כשה-Promise (או כל Promise מוקדם יותר בשרשרת) נדחה, ומקבל את השגיאה. .catch() אחד בסוף השרשרת מטפל בכשלונות מכל שלב שמעליו.

מה עושה Promise.all?

Promise.all([p1, p2, p3]) מקבל מערך של Promises ומחזיר Promise יחיד שמתממש עם מערך של כל הערכים, אבל רק אחרי שכל ה-Promises שהתקבלו התממשו. אם אחד מהם נדחה, הכול נדחה מיד. השתמשו ב-Promise.allSettled כשאתם רוצים את כל התוצאות בלי קשר לכשלונות.

כדאי להשתמש ב-Promises או ב-async/await?

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

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל