הבקשה נמצאת בתוך try, יש catch שמציג הודעת שגיאה, ובכל זאת הקוד ממשיך גם כשהכתובת לא נמצאה. זה קורה כי fetch מבדיל בין כישלון בביצוע הבקשה לבין תשובה שקוד הסטטוס שלה מציין שגיאה.
אם השרת החזיר 404, התקבלה תשובת HTTP. ה־Promise של fetch יכול להיפתר עם אובייקט Response, והקוד צריך לבדוק את הסטטוס. MDN מציינת במפורש שסטטוס שגיאה כמו 404 אינו גורם כשלעצמו לדחיית ה־Promise.
ארבעה מצבים שנראים אחרת בקוד
| מה קרה | מה הקוד צריך לבדוק |
|---|---|
השרת החזיר 404 או 500 | response.ok או response.status |
| הבקשה נכשלה בגלל בעיית רשת או מגבלת דפדפן | דחיית ה־Promise ב־catch |
| השרת החזיר גוף שאינו JSON צפוי | סוג התוכן ופענוח הגוף |
| הקוד ביטל בקשה שהמתינה יותר מדי | אות הביטול והשגיאה שהתקבלה |
המאפיין response.ok הוא true עבור סטטוסים בטווח 200-299. הוא מספר על הסטטוס, לא על תקינות המידע בתוך הגוף. גם תשובת 200 יכולה להכיל JSON שבור או אובייקט שחסרים בו שדות. תיעוד המאפיין מגדיר את הטווח הזה.
קריאת JSON היא פעולה שיכולה להיכשל
אחרי fetch יש בדרך כלל await response.json(). זו אינה בדיקת הצלחה: הפעולה קוראת את הגוף ומנסה לפענח אותו. אם שרת ביניים החזיר דף HTML, או אם הגוף ריק למרות שציפינו ל־JSON, הפענוח יכול להיכשל. תיעוד Response.json() מפרט את השגיאות האפשריות.
לכן עדיף לשמור על שני שלבים נפרדים: לבדוק את תשובת HTTP, ואז לקרוא את המידע שהאפליקציה מצפה לקבל. הודעת ״הכתובת לא נמצאה״ שימושית יותר למשתמש מהודעת פענוח שמזכירה תו מתוך דף HTML.
פונקציה קטנה עם התנהגות מוגדרת
הפונקציה הבאה מצפה ל־API שמחזיר JSON עם סוג תוכן application/json. היא מטפלת בסטטוס שגיאה, בתשובת 204 ללא תוכן ובהגבלת זמן. היא אינה ספריית בקשות כללית: למשל, היא אינה ממזגת אות ביטול חיצוני ואינה מנסה שוב אוטומטית.
כדי לנסות אותה, הפעילו את השרת מהמאמר הקודם, פתחו את הדף שהוא מגיש והדביקו את הקוד הבא ב־Console של הדפדפן:
class HttpError extends Error {
constructor(status, message) {
super(message);
this.name = "HttpError";
this.status = status;
}
}
async function requestJson(url, options = {}, timeoutMs = 8000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
const contentType = (response.headers.get("content-type") ?? "")
.split(";")[0].trim().toLowerCase();
const isJson = contentType === "application/json";
if (!response.ok) {
let message = `HTTP ${response.status}`;
if (isJson) {
try {
const body = await response.json();
if (typeof body?.error?.message === "string") {
message = body.error.message;
}
} catch (error) {
if (controller.signal.aborted) throw error;
}
}
throw new HttpError(response.status, message);
}
if (response.status === 204) return null;
if (!isJson) throw new Error("השרת החזיר סוג תוכן לא צפוי");
return await response.json();
} finally {
clearTimeout(timer);
}
}
ה־await לפני response.json() חשוב כאן. הוא משאיר את הטיימר פעיל גם בזמן קריאת הגוף, וה־finally מבטל אותו אחרי סיום הפעולה או כישלונה. AbortController מאפשר להעביר ל־Fetch אות ביטול; זהו המנגנון המתואר בתיעוד MDN.
בפענוח של הודעת שגיאה יש התנהגות מכוונת: אם הגוף אינו JSON תקין, עדיין נשמור את סטטוס HTTP. אם הפענוח נכשל מפני שהבקשה בוטלה, נעביר את שגיאת הביטול במקום להסתיר אותה.
מנסים הצלחה וכישלון
תחילה בקשו את רשימת הפוסטים:
requestJson("/api/posts")
.then(data => console.log(data.posts))
.catch(error => console.error(error.name, error.message));
התוצאה היא מערך, גם אם הוא ריק. כעת בקשו כתובת שאינה קיימת:
requestJson("/api/missing")
.catch(error => console.log(error.name, error.status));
הפלט הוא HttpError 404. עכשיו הכישלון מגיע ל־catch משום שהפונקציה שלנו בדקה את התשובה וזרקה שגיאה, לא משום ש־Fetch עושה זאת עבור כל סטטוס שגיאה.
אפשר גם לשלוח פוסט עם כותרת ריקה ולראות שההודעה מהשרת נשמרת:
requestJson("/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "" })
}).catch(error => console.log(error.status, error.message));
מה מציגים למשתמש?
בממשק כדאי לתרגם את סוג הכישלון להודעה מתאימה. שדה לא תקין דורש תיקון. בעיית חיבור יכולה להצדיק ניסיון נוסף. גוף לא צפוי מעיד על בעיה שדורשת בדיקה בצד המערכת.
function messageFor(error) {
if (error.name === "AbortError") {
return "הבקשה ארכה זמן רב מדי. בדוק אם הפעולה הושלמה לפני ניסיון נוסף.";
}
if (error instanceof HttpError) return error.message;
return "לא הצלחנו לקבל תשובה תקינה. התוכן שלך נשאר בטופס.";
}
המשפט האחרון מתאים רק אם הממשק אכן משאיר את התוכן בטופס. הודעת שגיאה היא חלק מההתנהגות של האפליקציה, ולכן היא צריכה לתאר את מה שקרה בפועל.
ביטול בקשה אינו ביטול הפעולה בשרת
יכול להיות שהשרת שמר פוסט, אבל החיבור נקטע לפני שהתשובה הגיעה. מבחינת הדפדפן הבקשה נכשלה; מבחינת השרת היא הושלמה. ביטול ההמתנה בדפדפן אינו מבטיח ביטול של העבודה בשרת.
לכן ניסיון חוזר אוטומטי של פעולה שיוצרת מידע עלול ליצור כפילות. לפני שמוסיפים אותו, צריך להגדיר איך מזהים את אותה פעולה ואיך מונעים ביצוע כפול. MDN מסבירה את מושג האידמפוטנטיות ב־HTTP: חזרה על בקשה אידמפוטנטית אמורה להשאיר את אותה השפעה מיועדת בשרת.
בדוגמת הפוסטים, כל POST תקין יוצר רשומה חדשה. זו התנהגות שקל לראות, ולכן אין בה ניסיון חוזר אוטומטי. השלב הבא הוא לתכנן מזהה פעולה או מנגנון אחר שמאפשר לדעת אם אותה שמירה כבר בוצעה.