Menu

טיפול בשגיאות async ב-JavaScript: try/catch, Promises ו-unhandledrejection

איך שגיאות באמת זורמות בקוד JavaScript אסינכרוני: try/catch עם async/await, .catch על promises, והמלכודות שבולעות כשלים בשקט.

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

שגיאות בקוד אסינכרוני לא מתנהגות כמו שגיאות סינכרוניות

ב-JavaScript סינכרוני, שגיאה שנזרקת מבעבעת במעלה מחסנית הקריאות עד שאיזה try/catch תופס אותה, או שהתוכנית קורסת. קוד אסינכרוני שובר את המודל הזה. עד שבקשת רשת נכשלת, הפונקציה שהתחילה אותה כבר חזרה מזמן. אין מחסנית קריאות לבעבע בה.

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

ה-try/catch רץ ומסתיים בלי תקלות. הדחייה קורית 50ms אחר כך, הרבה אחרי שבלוק ה-try הסתיים. שום דבר לא תופס אותה. זו המלכודת.

try/catch חוזר לעבוד עם await

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

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

פרט אחד ששווה לשים לב אליו: רק הקריאה שמחכים לה עם await מכוסה. אם מפעילים promise בלי לחכות לו, השגיאות עדיין בורחות.

הבאג הנפוץ ביותר: לשכוח את ה-await

אם קוראים לפונקציה async בלי await (או בלי להחזיר את ה-promise שלה), דחיות חומקות מה-try/catch שעוטף אותה:

בלוק ה-try מסתיים בהצלחה. הדחייה קורית ב-tick הבא, בלי שום דבר שיתפוס אותה. תראו בקונסול אזהרת "unhandled promise rejection".

התיקון תמיד זהה: לעשות await לקריאה, או return ל-promise כדי שהקורא יוכל לחכות לו.

.catch() הוא הצד השני של אותו מטבע

אפשר לטפל בדחיות בלי async/await על ידי שרשור של .catch():

.catch(fn) הוא קיצור של .then(undefined, fn). הוא מטפל בכל דחייה שקרתה קודם בשרשרת. .catch() בסוף שרשרת הוא המקבילה האסינכרונית ל-try/catch ברמה העליונה: קו ההגנה האחרון לפני שהדחייה הופכת ל"לא מטופלת".

אין בעיה לשלב בין שני הסגנונות. תבנית נפוצה היא להשתמש ב-async/await בתוך פונקציה ולתת לקורא לחבר .catch():

fetch לא נדחה בשגיאות HTTP

זה תופס את כולם לפחות פעם אחת. fetch נדחה רק בכשלים ברמת הרשת: חיפוש DNS נכשל, החיבור נדחה, הבקשה בוטלה. תגובת 404 או 500 נחשבת fetch מוצלח. ה-promise מתממש, פשוט עם תגובה שה-ok שלה הוא false.

אם אתם רוצים ששגיאות HTTP יגיעו לבלוק ה-catch, בדקו את res.ok וזרקו שגיאה במפורש:

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

Promise.all נכשל מהר, Promise.allSettled לא

Promise.all מקבל מערך של promises ומתממש עם מערך של תוצאות, אלא אם אחד מהם נדחה, ואז הוא נדחה מיד עם השגיאה הזו. ה-promises האחרים ממשיכים לרוץ, אבל התוצאות שלהם נזרקות.

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

allSettled אף פעם לא נדחה. כל רשומה היא {status: "fulfilled", value} או {status: "rejected", reason}.

זריקה מחדש ותפיסות ממוקדות

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

בליעה של כל שגיאה עם catch (err) {} ריק מסתירה באגים אמיתיים. תפסו את מה שאתם יכולים לטפל בו באופן משמעותי, וזרקו מחדש את השאר.

דחיות לא מטופלות הן רשת הביטחון שלכם

גם עם קוד זהיר, משהו יחמוק בסוף. גם Node.js וגם דפדפנים חושפים hook גלובלי לדחיות שאף אחד לא תפס:

// Browser
window.addEventListener("unhandledrejection", event => {
    console.error("לא מטופל:", event.reason);
    event.preventDefault(); // מדכא את אזהרת ברירת המחדל בקונסול
});

// Node.js
process.on("unhandledRejection", reason => {
    console.error("לא מטופל:", reason);
});

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

רשימת בדיקה מעשית

כשפונקציה async נוגעת במשהו שעלול להיכשל, שאלו את עצמכם:

  • האם כל await מסוכן נמצא בתוך try/catch, או שה-promise המוחזר מטופל אצל הקורא עם .catch()?
  • האם אני באמת עושה await לקריאה, או שבטעות עשיתי fire-and-forget?
  • ובמיוחד ב-fetch, האם אני בודק את res.ok לפני שאני סומך על התגובה?
  • כשמריצים דברים במקביל, האם Promise.all הוא הכלי הנכון, או שאני רוצה Promise.allSettled?
  • האם יש .catch() ברמה העליונה או מטפל unhandledrejection, כדי ששום דבר לא ייעלם בשקט?

תסדרו את חמשת אלה, והקוד האסינכרוני שלכם יפסיק להפתיע אתכם בשגיאות שנעלמות לתוך ה-event loop.

הבא בתור: ES Modules

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

שאלות נפוצות

איך מטפלים בשגיאות בפונקציה async?

עטפו את קריאות ה-await בבלוק try/catch. כל דחייה של promise שמחכים לו עם await הופכת לשגיאה שנזרקת, וה-catch מקבל אותה. אפשר גם לתת לשגיאה להתפשט ולטפל בה במקום הקריאה, עם .catch() על ה-promise שהפונקציה מחזירה.

למה ה-try/catch שלי לא תופס את השגיאה?

בדרך כלל כי השגיאה קורית בקוד שלא מחכים לו עם await. אם קוראים לפונקציה async בלי await (ובלי להחזיר את ה-promise שלה), כל דחייה בורחת מה-try/catch שעוטף אותה. תמיד עשו await או return ל-promise שממנו אתם רוצים לקבל שגיאות.

מה קורה אם promise נדחה ואף אחד לא תופס אותו?

מקבלים unhandled rejection. Node.js יורה אירוע unhandledRejection, ובגרסאות האחרונות מפיל את התהליך כברירת מחדל. דפדפנים יורים window.onunhandledrejection ורושמים אזהרה בקונסול. בכל מקרה, הוסיפו .catch() או טפלו בשגיאה עם try/catch סביב await.

איך Promise.all מטפל בשגיאות?

Promise.all נדחה ברגע שאחד מה-promises שקיבל נדחה. ה-promises האחרים ממשיכים לרוץ, אבל התוצאות שלהם נזרקות. אם אתם רוצים את כל התוצאות בלי קשר לכשלים, השתמשו ב-Promise.allSettled: הוא מתממש עם מערך של רשומות {status, value} או {status, reason}.

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

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

להתחיל