مدیریت خطا در async/await: فراتر از یک try/catch ساده
در درس قبل دیدید که async/await از بلوک آشنای try/catch برای مدیریت خطا استفاده میکند. اما در پروژههای واقعی، مدیریت خطا معمولاً پیچیدهتر از یک بلوک ساده است: گاهی لازم است خطاهای مختلف بهطور جداگانه بررسی شوند، گاهی باید کدی صرفنظر از نتیجه اجرا شود، و گاهی باید تصمیم گرفت که آیا خطا باید مدیریت شود یا به بالاتر منتقل شود. این درس به بررسی دقیقتر این الگوها میپردازد.
ساختار finally در کنار try/catch
درست مانند بلوک finally در زنجیرهی Promise، در async/await نیز میتوان از finally در انتهای try/catch استفاده کرد تا کدی صرفنظر از موفقیت یا شکست عملیات اجرا شود:
تفکیک خطاها بر اساس نوع
در سناریوهای واقعی، منابع مختلف خطا—مثل قطعی شبکه، پاسخ نامعتبر سرور، یا JSON خراب—نیاز به واکنشهای متفاوتی دارند. با بررسی نوع یا پیام خطا داخل بلوک catch، میتوان این تفکیک را انجام داد:
async function loadProfile(userId) {
try {
const response = await fetch(`/api/user/${userId}`);
if (!response.ok) {
throw new Error(`HTTP_ERROR_${response.status}`);
}
return await response.json();
} catch (error) {
if (error instanceof TypeError) {
// معمولاً به معنای قطعی اتصال یا مشکل شبکه است
console.log("مشکل در اتصال به سرور");
} else if (error.message.startsWith("HTTP_ERROR_")) {
console.log("سرور پاسخ نامعتبر برگرداند:", error.message);
} else {
console.log("خطای نامشخص:", error.message);
}
}
}
نکتهی مهم اینجاست که خطاهای شبکهای (مثل قطعی اتصال) توسط خود fetch بهصورت یک TypeError پرتاب میشوند؛ در حالی که خطاهای مربوط به کد وضعیت HTTP (مثل ۴۰۴ یا ۵۰۰) باید صراحتاً با throw توسط خود برنامهنویس تولید شوند، چون همانطور که در فصل ۱۸ دیدید، fetch بهخودیخود برای این کدهای وضعیت خطا پرتاب نمیکند.
خطا در چند عملیات مستقل با Promise.all
وقتی از Promise.all در کنار async/await استفاده میشود، کافی است یک try/catch اطراف آن نوشته شود؛ اگر هرکدام از Promiseهای داخل آرایه reject شوند، همان خطا به بلوک catch منتقل میشود:
async function loadDashboard() {
try {
const [user, orders] = await Promise.all([
fetch("/api/user").then((r) => r.json()),
fetch("/api/orders").then((r) => r.json()),
]);
console.log(user, orders);
} catch (error) {
console.log("حداقل یک درخواست شکست خورد:", error.message);
}
}
تصمیمگیری بین مدیریت محلی خطا و انتقال آن به بالا
گاهی تابعی که await در آن استفاده شده، خودش تصمیم نمیگیرد خطا را چگونه مدیریت کند؛ بلکه فقط اجازه میدهد خطا به تابع فراخواننده منتقل شود. این کار با نداشتن try/catch در آن تابع (یا throw مجدد خطا داخل catch) انجام میشود:
async function fetchUser(userId) {
const response = await fetch(`/api/user/${userId}`);
if (!response.ok) {
throw new Error(`کاربر پیدا نشد: ${response.status}`);
}
return response.json();
// این تابع عمداً try/catch ندارد؛ مدیریت خطا به فراخواننده سپرده شده
}
async function showUserProfile(userId) {
try {
const user = await fetchUser(userId);
console.log(user);
} catch (error) {
console.log("نمایش پیام خطا به کاربر:", error.message);
}
}
این الگو اجازه میدهد توابع سطح پایین (مثل fetchUser) صرفاً روی گرفتن داده تمرکز کنند، در حالی که تصمیم دربارهی نحوهی نمایش یا مدیریت خطا به لایهی بالاتری که با کاربر در تعامل است، سپرده میشود.
نکات کلیدی
- finally در async/await، دقیقاً مانند نسخهی Promise آن، صرفنظر از نتیجهی try/catch همیشه اجرا میشود.
- خطاهای شبکهای fetch بهصورت TypeError پرتاب میشوند؛ خطاهای مربوط به کد وضعیت HTTP باید صراحتاً throw شوند.
- یک try/catch اطراف Promise.all کافی است تا خطای هر یک از عملیاتهای موازی داخل آن گرفته شود.
- توابع سطح پایین میتوانند عمداً بدون try/catch نوشته شوند تا مدیریت نهایی خطا به تابع فراخواننده سپرده شود.
جمعبندی
مدیریت خطا در async/await فراتر از یک try/catch ساده است و شامل الگوهایی مثل استفاده از finally برای پاکسازی، تفکیک خطاها بر اساس نوع، و تصمیم آگاهانه دربارهی اینکه کدام لایه از برنامه مسئول مدیریت نهایی خطا باشد، میشود. تسلط بر این الگوها، تفاوت اصلی میان کدی که فقط در حالت موفق کار میکند و کدی که در پروژهی واقعی و در برابر خطاهای پیشبینینشده مقاوم است را رقم میزند.
