چند سال پیش، وسط یک پروژه‌ی فروشگاهی، مشتری پیام داد که «دکمه‌ی افزودن به سبد خرید روی آیفون کار نمی‌کند». روی دسکتاپ همه‌چیز بی‌نقص بود؛ روی اندروید هم مشکلی نداشت. دو ساعت وقت گذاشتم تا بفهمم یک شرط منطقی، در یکی از مسیرهای نادر، یک شیء undefined تولید می‌کرد و جاوااسکریپت ساکت، بدون هیچ پیامی، بقیه‌ی تابع را رها می‌کرد. آن روز برایم روشن شد که مدیریت خطا در جاوااسکریپت فقط try/catch گذاشتن دور کد نیست؛ یک مدل ذهنی کامل است که تفاوت بین اسکریپتی که در محیط تولید بی‌صدا می‌میرد و کدی که سال‌ها زنده می‌ماند را می‌سازد. در این مقاله، همان مسیری را می‌روم که امروز با تازه‌کارها طی می‌کنم: از انواع خطا و ابزارهای پایه تا خطاهای آسنکرون، استثناهای سفارشی، مدیریت خطای سراسری و الگوهای واقعی.

چرا مدیریت خطا در جاوااسکریپت متفاوت است؟

اگر تازه با جاوااسکریپت آشنا می‌شوید، اول آموزش جاوااسکریپت از صفر را بخوانید و بعد مفاهیم پایه جاوااسکریپت را مرور کنید. حالا سؤال اصلی: چرا مدیریت خطا در جاوااسکریپت از زبان‌های دیگر متفاوت است؟

در زبان‌هایی مثل PHP یا پایتون، خطا معمولاً باعث توقف اسکریپت می‌شود یا در لاگ سرور ثبت می‌شود. در جاوااسکریپت، خطا اغلب بی‌صدا است — یعنی خطا رخ می‌دهد، در کنسول لاگ می‌شود، ولی برنامه به کار خود ادامه می‌دهد. نتیجه: کاربر چیزی می‌بیند که منطقی نیست، ولی شما هیچ پیامی از خطا ندارید. سه دلیل این تفاوت را در پروژه‌های واقعی دیده‌ام:

  • محیط اجرا در دسترس کاربر است: برخلاف سرور که در کنترل شماست، جاوااسکریپت روی مرورگر کاربران اجرا می‌شود. اگر خطا را به‌درستی مدیریت نکنید، کاربر آن را می‌بیند ولی شما نه.
  • خطاها در callbackها بی‌صدا می‌روند: اگر یک callback خطا بدهد و در جایی مدیریت نشود، جاوااسکریپت گاهی خطا را خاموش می‌کند. این رفتار در رویدادها و async بسیار رایج است.
  • خطاهای نوع پویا: چون جاوااسکریپت نوع‌پویاست، بسیاری از خطاها (مثل دسترسی به property یک undefined) در زمان اجرا رخ می‌دهند، نه در زمان نوشتن کد. اصول این رفتار در مفاهیم پایه جاوااسکریپت توضیح داده شده است.
مدیریت خطا در جاوااسکریپت، نه برای «جلوگیری از خطا»، برای «دیدن خطا» و «رفع سریع خطا» است؛ اگر خطاها را نبینید، باگی که یک شب پروژه را می‌خواباند، ماه‌ها پیش‌تر بی‌صدا شروع شده است.

سه دسته خطا در جاوااسکریپت

خطاها در جاوااسکریپت به سه دسته تقسیم می‌شوند که هرکدام ابزار متفاوتی برای مدیریت دارند:

۱) خطای نحوی (Syntax Error)

خطایی که هنگام پارس شدن کد رخ می‌دهد — مثل فراموش‌کردن یک پرانتز بسته. این نوع خطا با try/catch قابل مدیریت نیست؛ چون کد اصلاً اجرا نمی‌شود. باید در زمان نوشتن کد، با ابزارهایی مثل ESLint یا TypeScript تشخیص داده شود:

// این کد اصلاً اجرا نمی‌شود
function broken() {
    console.log("hello";  // پرانتز بسته فراموش شده
}

۲) خطای زمان اجرا (Runtime Error)

خطایی که هنگام اجرای کد رخ می‌دهد — مثل خواندن property یک متغیر undefined، تقسیم بر صفر یا فراخوانی تابعی که وجود ندارد. این دسته، همان چیزی است که try/catch برای آن طراحی شده:

const user = undefined;
console.log(user.name); // TypeError: Cannot read property "name" of undefined

نوع‌های رایج خطای زمان اجرا که در پروژه‌های واقعی زیاد دیده‌ام:

نوع خطاعلت
TypeErrorعملیات روی نوع اشتباه — مثل undefined.name
ReferenceErrorاستفاده از متغیر تعریف‌نشده
RangeErrorمقدار خارج از محدوده — مثل آرایه با طول منفی
SyntaxErrorخطای پارس — مثل JSON نامعتبر
URIErrorخطا در تابع‌های URI — مثل decodeURIComponent
EvalErrorخطا در eval — امروز نادر

۳) خطای منطقی (Logical Error)

خطایی که جاوااسکریپت آن را نمی‌بیند — چون کد اجرا می‌شود ولی نتیجه اشتباه است. مثال: تابعی که باید مجموع اعداد را برگرداند، به‌خاطر نبود Number()، رشته‌ها را به هم می‌چسباند. این خطاها با هیچ try/catchای گرفته نمی‌شوند؛ فقط با تست، بازبینی کد و لاگ‌های دقیق کشف می‌شوند.

اگر با خطاهای رایج در مرورگر درگیر هستید، چگونه خطاهای جاوااسکریپت را در کنسول مرورگر پیدا کنیم مسیر تشخیص را گام‌به‌گام نشان می‌دهد.

try/catch/finally: ابزار پایه

ابزار پایه‌ی مدیریت خطا در جاوااسکریپت، بلوک try/catch است:

try {
    const data = JSON.parse(userInput);
    processData(data);
} catch (error) {
    console.error("Processing failed:", error.message);
    showErrorToUser("داده‌ی ورودی معتبر نیست");
} finally {
    hideLoading();
}

سه نکته‌ی مهم در استفاده از try/catch که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

۱) بلوک try را کوچک نگه دارید

اگر کل تابع را در try بگذارید، نمی‌دانید کدام خط مقصر بوده. بلوک را تا حد امکان کوچک نگه دارید تا دقیقاً بدانید چه چیزی ممکن است خطا بدهد:

// بد — try بزرگ
async function loadData() {
    try {
        const response = await fetch(url);
        const data = await response.json();
        const processed = transform(data);
        display(processed);
    } catch (error) {
        // کدام مرحله خطا داد؟
    }
}

// بهتر — try کوچک‌تر
async function loadData() {
    const response = await fetch(url);
    const data = await response.json();

    try {
        const processed = transform(data);
        display(processed);
    } catch (error) {
        // می‌دانیم خطا در transform یا display رخ داده
        console.error("Transform failed:", error);
    }
}

۲) در catch، خطا را خفه نکنید

// بد — خطا خفه شده
try {
    riskyOperation();
} catch (error) {
    // هیچ کاری نمی‌کنیم — باگ بی‌صدا
}

// خوب — لاگ یا پرتاب دوباره
try {
    riskyOperation();
} catch (error) {
    console.error("Operation failed:", error);
    throw error; // یا بازگرداندن مقدار جایگزین
}

۳) از finally برای آزادسازی منابع استفاده کنید

بلوک finally در هر شرایطی اجرا می‌شود — چه خطا رخ بدهد، چه نه. مناسب برای بستن لودرها، آزادسازی منابع و هر چیزی که «در هر حالت باید انجام شود»:

showLoading();

try {
    const data = await fetchData();
    display(data);
} catch (error) {
    showError(error);
} finally {
    hideLoading();
}

اگر با پایتون هم کار می‌کنید، همین ساختار در مدیریت خطا در پایتون با سینتکس متفاوتی بررسی شده است — مفاهیم مشترک‌اند ولی رفتار زبان‌ها در جزئیات فرق دارد.

بلوک try، مثل سبدی است که فقط چیزهای شکستنی را در آن می‌گذارید؛ اگر همه‌چیز را در آن بریزید، دیگر نمی‌دانید کدام شکست.

throw و شیء Error

گاهی خودتان باید خطا پرتاب کنید — مثلاً وقتی یک پارامتر نامعتبر است یا شرایطی برای ادامه وجود ندارد:

function divide(a, b) {
    if (b === 0) {
        throw new Error("Division by zero is not allowed");
    }
    return a / b;
}

سه نکته‌ی مهم در استفاده از throw که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

۱) همیشه Error پرتاب کنید، نه رشته

// بد
throw "Invalid input";

// خوب
throw new Error("Invalid input");

شیء Error شامل message، stack و name است. اگر رشته پرتاب کنید، stack trace از بین می‌رود و دیباگ بسیار سخت می‌شود. این تله در پروژه‌های واقعی به باگ‌های پیچیده منجر می‌شود.

۲) fail fast: در ابتدای تابع اعتبارسنجی کنید

function createUser(name, email, age) {
    if (!name || typeof name !== "string") {
        throw new TypeError("name must be a non-empty string");
    }
    if (!email || !email.includes("@")) {
        throw new Error("invalid email");
    }
    if (typeof age !== "number" || age < 0) {
        throw new RangeError("age must be a positive number");
    }

    // ادامه منطق
}

این الگو، جلوی خطاهای پنهان در ادامه‌ی تابع را می‌گیرد و فراخوان را سریع از مشکل مطلع می‌کند.

۳) throw در callback async

در callbackهای آسنکرون، throw رفتار متفاوتی دارد — چون خارج از دامنه‌ی try/catch فعلی اجرا می‌شود:

// اشتباه — try/catch آن را نمی‌گیرد
try {
    setTimeout(() => {
        throw new Error("Delayed error");
    }, 1000);
} catch (error) {
    // این catch اجرا نمی‌شود
}

این یک باگ کلاسیک در پروژه‌های واقعی است. برای مدیریت خطا در callbackهای آسنکرون، از Promise در جاوااسکریپت و async/await در جاوااسکریپت استفاده کنید که در ادامه به آن‌ها می‌رسم.

خطاهای سفارشی برای پروژه‌های جدی

در پروژه‌های بزرگ، استفاده از Error عمومی کافی نیست. اگر تابع شما Error پرتاب کند، فراخوان نمی‌داند که خطا مربوط به اعتبارسنجی است یا خطای داخلی. راه‌حل، ساخت استثناهای سفارشی است:

class AppError extends Error {
    constructor(message, code) {
        super(message);
        this.name = "AppError";
        this.code = code;
    }
}

class ValidationError extends AppError {
    constructor(field, message) {
        super(message, "VALIDATION_ERROR");
        this.field = field;
    }
}

class NotFoundError extends AppError {
    constructor(resource) {
        super(`${resource} not found`, "NOT_FOUND");
        this.resource = resource;
    }
}

class PaymentError extends AppError {
    constructor(message, transactionId) {
        super(message, "PAYMENT_ERROR");
        this.transactionId = transactionId;
    }
}

و در کد کاربردی:

async function createOrder(orderData) {
    if (!orderData.items?.length) {
        throw new ValidationError("items", "سفارش باید حداقل یک قلم داشته باشد");
    }

    const user = await findUser(orderData.userId);
    if (!user) {
        throw new NotFoundError("User");
    }

    const payment = await processPayment(orderData);
    if (!payment.success) {
        throw new PaymentError("پرداخت ناموفق", payment.transactionId);
    }

    return createOrderRecord(orderData, payment);
}

سه مزیت این الگو که در پروژه‌های واقعی تجربه کرده‌ام:

  • فیلتر کردن خطاها: لایه‌ی بالایی می‌تواند catch (e) { if (e instanceof AppError) ... } بزند و خطاهای سیستمی (مثل TypeError داخلی) را جدا کند.
  • اطلاعات اضافه: هر خطا می‌تواند فیلدهای خودش را داشته باشد — مثل field برای فرم یا transactionId برای پرداخت.
  • پیام مناسب به کاربر: لایه‌ی نمایش می‌تواند بر اساس instanceof، پیام مناسب به کاربر نشان دهد، نه پیام فنی خام.

اصول مشابه این رویکرد در شی‌گرایی در جاوااسکریپت با جزئیات بیشتری آمده است — چون استثناها هم یک نوع شیء هستند و از همان مفاهیم وراثت استفاده می‌کنند.

خطاهای آسنکرون: Promise و async/await

بزرگ‌ترین چالش مدیریت خطا در جاوااسکریپت، خطاهای آسنکرون هستند. قبل از ES2017، خطاها در callbackها غالباً بی‌صدا می‌رفتند:

// خطا در callback، بی‌صدا
someAsyncFunction(function (result) {
    throw new Error("Something failed");  // جایی گرفته نمی‌شود
});

۱) مدیریت خطا در Promise

Promise خطاها را از طریق reject منتقل می‌کند که با .catch گرفته می‌شود:

fetchUser(42)
    .then((user) => fetchOrders(user.id))
    .then((orders) => displayOrders(orders))
    .catch((error) => {
        console.error("Failed to load data:", error);
        showErrorToUser("خطا در دریافت داده");
    });

یک نکته‌ی حیاتی: Promise بدون catch، یک باگ بی‌صدا است. اگر خطایی رخ دهد و هیچ catch نداشته باشید، خطا در کنسول ظاهر می‌شود ولی برنامه شما با داده‌ی ناقص ادامه می‌دهد. اصول کامل Promise در Promise در جاوااسکریپت آمده است.

۲) مدیریت خطا در async/await

با async/await، مدیریت خطا با try/catch انجام می‌شود که برای توسعه‌دهندگان سایر زبان‌ها طبیعی‌تر است:

async function loadData(userId) {
    try {
        const user = await fetchUser(userId);
        const orders = await fetchOrders(user.id);
        return { user, orders };
    } catch (error) {
        console.error("Failed to load data:", error);
        throw error;
    }
}

سه نکته‌ی مهم در async/await که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • try/catch، فقط دور کدهای async کار می‌کند: خطاهای درون Promiseای که await نشده باشد، به catch نمی‌رسند.
  • Promise.all را در try بگذارید: خطای هرکدام از Promiseها باعث Reject شدن کل می‌شود.
  • در حلقه با for...of، هر iteration جدا catch می‌شود: اگر می‌خواهید خطای یک iteration بقیه را متوقف نکند، داخل حلقه try/catch بگذارید.

اصول کامل async/await در async/await در جاوااسکریپت آمده است. و اگر با ES6 آشنا نیستید، آموزش ES6 در جاوااسکریپت پیش‌نیاز خوبی است — چون خیلی از الگوهای مدرن خطا، به امکانات ES6 تکیه دارند.

خطاهای آسنکرون، پاشنه‌ی آشیل جاوااسکریپت هستند؛ اگر آن‌ها را نبینید، شبیه رانندگی در شب بدون چراغ هستید — تا وقتی به دیوار نخورید، نمی‌دانید مسیری را اشتباه رفته‌اید.

مدیریت خطا در fetch API

fetch API، یکی از پرتکرارترین منابع خطاهای بی‌صدا در پروژه‌های واقعی است. سه لایه‌ی خطا در fetch وجود دارد که باید همه‌شان را مدیریت کنید:

async function apiFetch(url, options = {}) {
    try {
        const response = await fetch(url, options);

        // لایه ۱: خطای HTTP (۴xx و ۵xx) — fetch خودش Reject نمی‌کند
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        // لایه ۲: خطای پارس JSON
        try {
            return await response.json();
        } catch (parseError) {
            throw new Error(`Invalid JSON response: ${parseError.message}`);
        }
    } catch (error) {
        // لایه ۳: خطای شبکه (DNS، CORS، قطع اتصال)
        console.error(`Fetch failed for ${url}:`, error.message);
        throw error;
    }
}

مهم‌ترین نکته‌ای که در پروژه‌های واقعی به آن رسیده‌ام: fetch روی کدهای ۴xx و ۵xx Reject نمی‌شود. یعنی اگر ۴۰۴ برنگردد، شما آن را به‌عنوان موفقیت می‌گیرید. باید همیشه response.ok را بررسی کنید. اصول کامل fetch در fetch API در جاوااسکریپت آمده است.

خطای سراسری: window.onerror

برای خطاهایی که از دست catch خارج می‌شوند، می‌توانید از مدیریت خطای سراسری استفاده کنید:

window.addEventListener("error", (event) => {
    console.error("Global error:", event.error);

    // ارسال به سرور
    fetch("/api/log-error", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
            message: event.message,
            filename: event.filename,
            lineno: event.lineno,
            colno: event.colno,
            stack: event.error?.stack,
        }),
    }).catch(() => {}); // جلوگیری از حلقه‌ی بی‌پایان
});

و برای خطاهای Promise که مدیریت نشده‌اند:

window.addEventListener("unhandledrejection", (event) => {
    console.error("Unhandled promise rejection:", event.reason);

    fetch("/api/log-error", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
            type: "unhandledRejection",
            reason: event.reason?.message || String(event.reason),
            stack: event.reason?.stack,
        }),
    }).catch(() => {});
});

این دو listener در پروژه‌های واقعی، بسیار ارزشمند هستند — چون خطاهایی را می‌گیرند که با try/catch نگرفته‌اید. تجربه‌ی من: با اضافه‌کردن این دو، تعداد باگ‌های بی‌صدا در پروژه‌های پربازدید تا هفتاد درصد کاهش یافت. اگر با سمت سرور هم کار می‌کنید، اصول دریافت و ذخیره‌سازی این لاگ‌ها در امنیت API آمده است — چون لاگ‌ها هم باید امنیت‌شان رعایت شود.

الگوهای واقعی در پروژه‌ها

چند الگویی که با مدیریت خطا در پروژه‌های واقعی به ذهنیت ثابت من تبدیل شده‌اند:

الگوی اول: Wrapper مرکزی API

class ApiClient {
    constructor(baseURL, options = {}) {
        this.baseURL = baseURL;
        this.options = options;
    }

    async request(path, config = {}) {
        const url = `${this.baseURL}${path}`;

        try {
            const response = await fetch(url, {
                ...this.options,
                ...config,
                headers: {
                    "Content-Type": "application/json",
                    ...this.options.headers,
                    ...config.headers,
                },
            });

            if (!response.ok) {
                const errorBody = await response.json().catch(() => ({}));
                throw new ApiError(
                    errorBody.message || `HTTP ${response.status}`,
                    response.status,
                    errorBody.code,
                );
            }

            return response.status === 204 ? null : response.json();
        } catch (error) {
            if (error instanceof ApiError) throw error;
            throw new NetworkError(error.message);
        }
    }
}

الگوی دوم: Retry با تأخیر نمایی

async function fetchWithRetry(url, options = {}, retries = 3) {
    for (let attempt = 0; attempt < retries; attempt++) {
        try {
            const response = await fetch(url, options);

            // فقط خطاهای گذرا را retry کن (۵xx و ۴۲۹)
            if (response.status >= 500 || response.status === 429) {
                throw new Error(`Transient error: ${response.status}`);
            }

            if (!response.ok) {
                throw new Error(`Permanent error: ${response.status}`);
            }

            return response;
        } catch (error) {
            const isLastAttempt = attempt === retries - 1;
            const isPermanent = error.message.includes("Permanent");

            if (isLastAttempt || isPermanent) throw error;

            await new Promise((resolve) =>
                setTimeout(resolve, 1000 * Math.pow(2, attempt))
            );
        }
    }
}

نکته‌ی کلیدی این الگو: همه‌ی خطاها را retry نکنید. خطاهای ۴xx معمولاً دائمی هستند و retry کردنشان فقط بار سرور را بالا می‌برد.

الگوی سوم: Graceful Degradation

async function loadUserRecommendations(userId) {
    try {
        return await fetchRecommendations(userId);
    } catch (error) {
        console.warn("Recommendations failed, using fallback:", error);
        try {
            return await fetchPopularItems();
        } catch (fallbackError) {
            console.error("Fallback also failed:", fallbackError);
            return []; // مقدار نهایی — تجربه‌ی کاربری حفظ می‌شود
        }
    }
}

این الگو در پروژه‌های واقعی، تفاوت بین یک صفحه‌ی کاربری که «همه‌چیز خوب است» و یک صفحه‌ی خطا را می‌سازد. حتی اگر بخشی از داده بارگیری نشود، بقیه‌ی صفحه سالم می‌ماند.

الگوی چهارم: Timeout برای عملیات

async function fetchWithTimeout(url, options = {}, timeout = 5000) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeout);

    try {
        const response = await fetch(url, {
            ...options,
            signal: controller.signal,
        });
        return response;
    } catch (error) {
        if (error.name === "AbortError") {
            throw new Error(`Request timed out after ${timeout}ms`);
        }
        throw error;
    } finally {
        clearTimeout(timer);
    }
}

این الگو، از یک باگ پنهان جلوگیری می‌کند: درخواست‌هایی که ممکن است ساعت‌ها معلق بمانند و تجربه‌ی کاربری را خراب کنند.

لاگ و پایش خطا

در پروژه‌های واقعی، مدیریت خطا بدون لاگ و پایش، ناقص است. سه ابزار که در پروژه‌هایم به‌کار می‌برم:

۱) لاگ سمت کلاینت به سرور

class ErrorLogger {
    static async log(error, context = {}) {
        const payload = {
            message: error.message,
            stack: error.stack,
            name: error.name,
            url: window.location.href,
            userAgent: navigator.userAgent,
            timestamp: new Date().toISOString(),
            context,
        };

        try {
            await fetch("/api/errors", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify(payload),
                keepalive: true,  // ارسال حتی هنگام بستن صفحه
            });
        } catch (loggingError) {
            console.error("Failed to log error:", loggingError);
        }
    }
}

// استفاده
try {
    riskyOperation();
} catch (error) {
    ErrorLogger.log(error, { feature: "checkout" });
}

۲) تفکیک خطاهای محیط

const isProduction = process.env.NODE_ENV === "production";

function logError(error, context) {
    if (isProduction) {
        // فقط در محیط تولید به سرور بفرست
        ErrorLogger.log(error, context);
    } else {
        // در توسعه، کامل لاگ کن
        console.error("Error:", error, "Context:", context);
    }
}

۳) فیلتر کردن خطاهای نویز

در پروژه‌های واقعی، بعضی خطاها نویز هستند — مثل خطای افزونه‌های مرورگر کاربران یا خطای اسکریپت‌های تبلیغاتی. این‌ها را فیلتر کنید تا لاگ شما قابل استفاده بماند:

const NOISE_PATTERNS = [
    /chrome-extension:\/\//,
    /moz-extension:\/\//,
    /ResizeObserver loop limit exceeded/,
];

function isNoise(error) {
    const message = error.message || "";
    const stack = error.stack || "";
    return NOISE_PATTERNS.some((pattern) =>
        pattern.test(message) || pattern.test(stack)
    );
}

window.addEventListener("error", (event) => {
    if (event.error && !isNoise(event.error)) {
        ErrorLogger.log(event.error);
    }
});

تجربه‌ی من: بدون این فیلتر، سیستم لاگ شما از نویز پر می‌شود و خطاهای واقعی در آن گم می‌شوند. ابزارهای تخصصی مثل Sentry این فیلتر را خودکار انجام می‌دهند و برای پروژه‌های جدی، سرمایه‌گذاری رویشان ارزشش را دارد. اصول مرتبط با امنیت این لاگ‌ها در هدرهای امنیتی HTTP و امنیت API آمده است — چون لاگ‌ها هم باید محافظت شوند.

اشتباهاتی که در پروژه‌های واقعی دیده‌ام

در بازبینی کد پروژه‌های مختلف، این اشتباهات را زیاد دیده‌ام:

  • بلوک catch خالی: مهم‌ترین و پرتکرارترین اشتباه. catch (e) {} خطا را خفه می‌کند و کد شما در روز بحران، بی‌صدا می‌میرد. حداقل لاگ کنید.
  • پرتاب رشته به‌جای Error: throw "error" به‌جای throw new Error("error"). نتیجه: نبود stack trace و دیباگ سخت.
  • نبود catch در Promise: هر .then باید .catch داشته باشد. بدون آن، خطاها به unhandledrejection می‌روند.
  • throw در callback async بدون مدیریت: setTimeout(() => { throw new Error() }) در try/catch اطرافش گرفته نمی‌شود. این باگ در پروژه‌های واقعی باعث خطاهای بی‌صدا می‌شود.
  • blur کردن علت اصلی خطا: اگر در catch، یک خطای جدید پرتاب می‌کنید و خطای اصلی را درونش قرار نمی‌دهید، stack trace از دست می‌رود. از { cause: error } یا ذخیره‌ی originalError استفاده کنید.
  • نبود بررسی response.ok در fetch: fetch روی کدهای ۴xx و ۵xx Reject نمی‌شود. اگر بررسی نکنید، خطا را به‌عنوان موفقیت می‌گیرید.
  • نبود timeout در درخواست‌ها: درخواست بدون timeout، ممکن است بی‌پایان معلق بماند. از AbortSignal.timeout استفاده کنید.
  • بازگرداندن undefined به‌جای throw: اگر تابع خطا دارد، throw کنید نه return undefined. باگ‌های پنهان زمانی رخ می‌دهند که فراخوان انتظار داده دارد ولی undefined می‌گیرد.
  • نبود مدیریت خطای سراسری: بدون window.onerror و unhandledrejection، خطاهایی که از دست catch خارج می‌شوند، هیچ‌وقت نمی‌بینید.
  • نبود مستندسازی الگوی خطا: اگر تیم شما استثناهای سفارشی دارد، مستندسازی نکنید که کدام لایه کدام خطا را پرتاب می‌کند، تیم جدید نمی‌تواند راحت کار کند.
  • مصرف زیاد حافظه در آرایه‌ی خطاها: اگر خطاها را در آرایه‌ای جمع می‌کنید و پاک نمی‌کنید، حافظه پر می‌شود. از یک صف محدود یا سیستم لاگ تخصصی استفاده کنید.
  • نبود تفکیک محیط: در توسعه، خطاها را کامل لاگ کنید؛ در تولید، فقط به سرور بفرستید. ارسال همه‌ی خطاها در توسعه، سرور لاگ را پر می‌کند.
  • قربانی‌کردن خوانایی برای مدیریت اضافه: بعضی تیم‌ها هر تابع را در try/catch می‌پیچند، حتی وقتی واقعاً نیازی نیست. مدیریت خطا باید آگاهانه باشد، نه به‌عنوان یک آیین.
  • بازتاب اطلاعات حساس در پیام خطا: پیام خطا نباید شامل رمز، توکن یا اطلاعات کاربر باشد. اصول کامل این محافظت در امنیت وب چیست آمده است.

یک توصیه‌ی عملی از تجربه: در پروژه‌های جدید، از روز اول یک تابع مشترک logError بسازید که خطاها را به‌شکل استاندارد لاگ می‌کند. همه‌ی catchها از این تابع استفاده کنند. این انضباط کوچک، جلوی بسیاری از خطاهای بی‌صدا را می‌گیرد و در روز بحران، یک نقطه‌ی واحد برای بررسی خطاها به شما می‌دهد. اگر با کدبازبینی و ابزارهای تخصصی کار می‌کنید، بهینه‌سازی جاوااسکریپت نکات مکملی در مورد کارایی این تابع‌ها دارد. و برای سناریوهای آسنکرون پیشرفته، async/await در جاوااسکریپت مرجع عمیق‌تری است.

سخن آخر

مدیریت خطا در جاوااسکریپت، از یک try/catch ساده شروع می‌شود ولی در پروژه‌های واقعی، به یک مدل ذهنی کامل تبدیل می‌شود که پایداری و قابلیت دیباگ برنامه شما را تعیین می‌کند. سه نکته‌ی اصلی که در این مقاله به آن‌ها رسیدیم: اول، خطاها در جاوااسکریپت اغلب بی‌صدا هستند — بدون مدیریت دقیق، باگ‌هایی که ماه‌ها پیش شروع شده‌اند، فقط در روز بحران خودشان را نشان می‌دهند؛ دوم، خطاهای آسنکرون، بزرگ‌ترین دام جاوااسکریپت هستند — Promise بدون catch، callback بدون مدیریت و fetch بدون بررسی response.ok سه منبع اصلی خطاهای بی‌صدا هستند؛ سوم، لاگ و پایش خطا، بخش جدانشدنی مدیریت خطاست — بدون window.onerror و یک سیستم لاگ متمرکز، خطاهایی که از دست catch خارج می‌شوند را هرگز نمی‌بینید.

اگر امروز می‌خواهید در مدیریت خطا ماهر شوید، سه کار کوچک پیشنهاد می‌کنم: در پروژه‌ی فعلی‌تان همه‌ی بلوک‌های catch خالی را پیدا کنید و به هرکدام یک لاگ اضافه کنید؛ یک تابع مشترک apiFetch بسازید که بررسی response.ok، timeout و try/catch داشته باشد و همه‌ی درخواست‌ها از آن رد شوند؛ و دو listener سراسری window.onerror و unhandledrejection را اضافه کنید که خطاها را به سرور بفرستند. همین سه کار، در بیشتر پروژه‌ها، تعداد باگ‌های بی‌صدا را به‌شدت کاهش می‌دهد. مسیر طبیعی بعدی، async/await در جاوااسکریپت، fetch API در جاوااسکریپت و Promise در جاوااسکریپت است — چون سه‌گانه‌ی خطا، Promise و fetch، سه ستون جدانشدنی کد آسنکرون حرفه‌ای هستند. اگر تجربه‌ای از مدیریت خطا در پروژه‌های خودتان دارید — مخصوصاً اگر با یک باگ بی‌صدا یا خطای سراسری روبرو شده‌اید — در دیدگاه‌ها بنویسید؛ همین نکته‌های میدانی، برای خواننده‌ی بعدی از هر مستند رسمی ارزشمندتر است. 🛡️