اولین بار که یک Uncaught TypeError در JavaScript واقعاً وقتم را گرفت، در یک فروشگاه آنلاین بود. همه‌چیز در مرورگر خودم بی‌نقص کار می‌کرد، ولی مشتری‌ها گزارش می‌دادند که سبد خرید بالا نمی‌آید. در کنسول، یک پیام آشنا دیده می‌شد: Uncaught TypeError: Cannot read property "length" of undefined. تا آن روز، این خطا را یک مشکل ساده می‌دانستم که با یک if حل می‌شود. آن روز فهمیدم که Uncaught TypeError در JavaScript، در باطن، یک پنجره است به سمت معماری داده، مرزهای بین ماژول‌ها، و تفکر مرزی در کد asynchronous. این خطا، در ظاهر یک type system را نشان می‌دهد، ولی در واقع، یک سیگنال از فرض‌های نادرست درباره داده است.

خطای Uncaught TypeError در JavaScript دقیقاً چیست؟

JavaScript یک زبان با نوع‌شناسی پویا (dynamic typing) است که در آن نوع متغیرها در زمان اجرا تعیین می‌شود. وقتی کد شما سعی می‌کند عملیاتی روی یک مقدار انجام دهد که با نوع آن سازگار نیست، JavaScript خطای TypeError را مطرح می‌کند. وقتی این خطا در هیچ try/catch گرفته نشود، به‌عنوان Uncaught TypeError در کنسول مرورگر ظاهر می‌شود و اجرای اسکریپت در آن نقطه متوقف می‌شود:

Uncaught TypeError: Cannot read properties of undefined (reading "name")
    at getUserName (app.js:42)
    at HTMLButtonElement.onclick (index.html:15)

پیام خطا معمولاً چند بخش دارد: نوع خطا (TypeError)، توضیح دقیق، مسیر فایل و شماره خط، و در ادامه، stack trace که مسیر فراخوانی را نشان می‌دهد. ترکیب این داده‌ها، جهت تشخیص را تعیین می‌کند.

نکته‌ی مهم این است که خطای TypeError در JavaScript از نوع Error است، نه SyntaxError. بنابراین در زمان parse کد تشخیص داده نمی‌شود، بلکه در زمان اجرا رخ می‌دهد. به همین دلیل، خطاهای TypeError می‌توانند در محیط‌های مختلف (development، staging، production) رفتار متفاوتی داشته باشند، چون داده‌ی ورودی می‌تواند متفاوت باشد.

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

Uncaught TypeError یک شکایت از نوع است، نه از کد. JavaScript می‌گوید این عملیات، روی این مقدار قابل انجام نیست. کد شما فرض کرده چیزی که در دست دارد، از نوع دیگری است.

Uncaught به چه معناست؟

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

  1. پرتاب خطا (throw): کد یا موتور JavaScript یک شیء Error تولید می‌کند.
  2. جستجوی catch: مرورگر به دنبال یک بلوک try/catch در مسیر اجرا می‌گردد که این خطا را بگیرد.
  3. انتشار (propagation): اگر بلوک try/catch پیدا نشود، خطا در stack فراخوانی به بالا می‌رود.
  4. Uncaught: اگر در هیچ نقطه‌ای گرفته نشود، خطا به‌عنوان Uncaught در کنسول نمایش داده می‌شود و اجرای اسکریپت متوقف می‌شود.

نکته‌ی ظریف: Uncaught نشان می‌دهد که کد شما این خطا را مدیریت نکرده است. در بعضی موارد، این رفتار مطلوب است چون خطاهای برنامه‌ای باید افشا شوند. ولی در محیط production، Uncaught می‌تواند به تجربه‌ی کاربری ضعیف و قطع سرویس منجر شود.

برای مدیریت این خطاها، می‌توانید از try/catch یا از event listener سراسری window.addEventListener("error", ...) استفاده کنید. رویکرد دوم، در مدیریت خطا در جاوااسکریپت به‌تفصیل آمده است.

تفاوت Uncaught TypeError و TypeError

در JavaScript، TypeError و Uncaught TypeError دو مفهوم متفاوت هستند که با یکدیگر اشتباه گرفته می‌شوند:

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

Uncaught TypeError: همان TypeError است، ولی در هیچ نقطه‌ای از کد گرفته نشده. یعنی به‌عنوان یک خطای مدیریت‌نشده در کنسول ظاهر می‌شود.

تفاوت عملی: اگر از try/catch استفاده کنید، خطا دیگر Uncaught نیست:

try {
    const name = user.name;
    console.log(name);
} catch (error) {
    console.error("Caught:", error.message);
    // این خطا دیگر Uncaught نیست
}

نکته‌ی مهم: TypeError تنها یکی از انواع خطاهای Error در JavaScript است. سایر انواع مهم:

نوع خطا موضوع شکایت مثال
TypeError نوع مقدار با عملیات ناسازگار است undefined.name
ReferenceError متغیر در هیچ scope وجود ندارد nonExistentVar
SyntaxError خطای نگارشی در کد const x = ;
RangeError مقدار خارج از دامنه مجاز new Array(-1)
URIError خطای پردازش URI decodeURI("%")

جزئیات کامل هر یک از این خطاها در خطای TypeError در جاوااسکریپت، خطای ReferenceError در جاوااسکریپت، و خطای SyntaxError در جاوااسکریپت آمده است.

چرا JavaScript این خطا را مطرح می‌کند؟

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

یک: صراحت بهتر از ابهام است. اگر JavaScript به‌جای خطا، مقدار undefined یا null برمی‌گرداند، باگ‌ها بی‌صدا در محاسبات بعدی پخش می‌شدند. JavaScript ترجیح می‌دهد این خطا را در همان نقطه مطرح کند.

دو: رفتار امن در برابر باگ. دسترسی به property یک undefined، نشانه‌ی یک فرض نادرست در کد است. JavaScript این فرض را در همان لحظه افشا می‌کند تا دیباگ ساده‌تر شود.

سه: امکان مدیریت صریح. JavaScript با مفهوم try/catch و متد Error.prototype، امکان مدیریت دقیق این خطاها را فراهم می‌کند. همچنین، با ابزارهای مدرن مثل Optional Chaining، می‌توانید کد مقاوم‌تری بنویسید.

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

ده علت رایج خطای Uncaught TypeError

در تجربه‌ی من روی صدها پروژه‌ی JavaScript، Uncaught TypeError از ده علت مشخص می‌آید. شناخت این علت‌ها، تشخیص را در چند ثانیه ممکن می‌کند.

  1. دسترسی به property یک undefined یا null: شایع‌ترین علت.
  2. فراخوانی متدی که وجود ندارد: X is not a function.
  3. تخصیص property به یک undefined: Cannot set property.
  4. دسترسی به DOM قبل از آمادگی: عنصر مورد نظر وجود ندارد.
  5. داده‌ی asynchronous که هنوز نرسیده: کد روی response خالی کار می‌کند.
  6. مشکل در import ماژول‌ها: نام export شده اشتباه است.
  7. event handlerها با این اشتباه: استفاده‌ی نادرست از this.
  8. map یا forEach روی آرایه‌ی undefined: منبع داده معتبر نیست.
  9. TypeError در فریم‌ورک‌ها: React، Vue، Angular با lifecycle.
  10. TypeError ناشی از کتابخانه‌های شخص ثالث: نسخه‌ی ناسازگار.

هر علت، نشانه‌های مخصوص به خود و راه‌حل اختصاصی دارد. در بخش‌های بعدی، هر علت را جداگانه باز می‌کنم.

Cannot read properties of undefined یا null

شایع‌ترین شکل Uncaught TypeError، دسترسی به property یک undefined یا null است:

const user = getUser();
console.log(user.name);
// Uncaught TypeError: Cannot read properties of undefined (reading "name")

در این مثال، getUser() مقدار undefined برگردانده، ولی کد فرض کرده یک شیء معتبر است.

راه‌حل‌ها:

راه اول: بررسی صریح

const user = getUser();
if (user) {
    console.log(user.name);
} else {
    console.log("User not found");
}

راه دوم: استفاده از Optional Chaining

const user = getUser();
console.log(user?.name ?? "Guest");

راه سوم: مقادیر پیش‌فرض

const user = getUser() ?? {};
console.log(user.name ?? "Guest");

نکته‌ی ظریف: خطای Cannot read properties of undefined در مرورگرهای مختلف پیام متفاوتی دارد. در V8 (Chrome/Node.js) پیام دقیق است، در SpiderMonkey (Firefox) و JavaScriptCore (Safari) ممکن است پیام متفاوتی ببینید. ولی مفهوم در همه یکسان است.

برای درک تفاوت دقیق undefined و null، خطای null در جاوااسکریپت و خطای undefined در جاوااسکریپت را ببینید.

X is not a function

دومین شکل شایع Uncaught TypeError، فراخوانی چیزی است که به‌عنوان تابع در دسترس نیست:

const data = { name: "Ali" };
data.map(item => item);
// Uncaught TypeError: data.map is not a function

دلایل رایج:

  • مقدار انتظاری، شیء است ولی آرایه نیست.
  • تابع مورد نظر وجود ندارد. مثلاً array.filter روی یک شیء.
  • import ماژول اشتباه است. مثلاً import { helper } from "module" ولی helper در آن ماژول نیست.
  • تایپی در نام متد: array.map به‌جای array.map (اگرچه این مورد نادر است چون نام‌ها شبیه هستند).

راه‌حل: بررسی نوع قبل از فراخوانی:

if (Array.isArray(data)) {
    data.map(item => item);
} else {
    console.error("Expected array, got:", typeof data);
}

در پروژه‌های بزرگ، این مشکل معمولاً از داده‌ی خارجی (API، فرم، storage) می‌آید. راه‌حل ریشه‌ای، اعتبارسنجی داده در لایه‌ی ورودی است.

Cannot set property of undefined

سومین شکل، تلاش برای تخصیص property به یک undefined:

const config = getConfig();
config.theme = "dark";
// Uncaught TypeError: Cannot set properties of undefined (setting "theme")

در این مثال، getConfig() مقدار undefined برگردانده و تخصیص property روی undefined خطا می‌دهد.

راه‌حل: اطمینان از وجود شیء قبل از تخصیص:

const config = getConfig() ?? {};
config.theme = "dark";

این رویکرد، در تنظیمات پویا و state management بسیار مفید است.

TypeError در دسترسی به DOM

چهارمین منبع، مربوط به دسترسی به DOM است. خطای کلاسیک:

const button = document.querySelector("#submit");
button.addEventListener("click", handleClick);
// Uncaught TypeError: Cannot read properties of null (reading "addEventListener")

در این مثال، querySelector مقدار null برگردانده چون عنصر مورد نظر در صفحه وجود ندارد.

دلایل رایج:

  • سلکتور اشتباه: id یا class با آنچه در HTML است تطبیق ندارد.
  • اسکریپت قبل از DOM اجرا می‌شود: کد در <head> بدون defer.
  • عنصر در iframe یا Shadow DOM: عنصر در document ریشه نیست.
  • محتوای داینامیک: عنصر هنوز render نشده.

راه‌حل: بررسی صریح قبل از دسترسی:

const button = document.querySelector("#submit");
if (button) {
    button.addEventListener("click", handleClick);
} else {
    console.warn("Button not found: #submit");
}

برای مدیریت محتوای داینامیک، از MutationObserver یا DOMContentLoaded استفاده کنید. مبانی DOM در کار با DOM در جاوااسکریپت آمده است.

TypeError در کدهای asynchronous

پنجمین منبع، مربوط به کدهای asynchronous است. الگوی کلاسیک:

let userData = null;

fetch("/api/user")
    .then(response => response.json())
    .then(data => {
        userData = data;
    });

console.log(userData.name);
// Uncaught TypeError: Cannot read properties of null (reading "name")

در این مثال، console.log به‌طور همزمان با fetch اجرا می‌شود، در حالی که داده هنوز بارگذاری نشده است. راه‌حل: مدیریت داده در then یا async/await:

async function loadUser() {
    try {
        const response = await fetch("/api/user");
        const userData = await response.json();
        console.log(userData.name);
    } catch (error) {
        console.error("Failed to load user:", error);
    }
}

loadUser();

نکته‌ی ظریف: در JavaScript، خطاهای داخل then یا async به‌عنوان Uncaught Promise Rejection ظاهر می‌شوند، نه Uncaught TypeError. این تفاوت مهم است چون روش مدیریتش متفاوت است. جزئیات کامل در Promise در جاوااسکریپت و async و await در جاوااسکریپت آمده است.

TypeError در import و ماژول‌ها

ششمین منبع، مربوط به import و ماژول‌ها است. با ظهور ES Modules، این نوع خطا شایع‌تر شده:

import { helper } from "./utils.js";
helper();
// Uncaught TypeError: helper is not a function

دلایل رایج:

  • نام export اشتباه است: تابع با نام دیگری export شده.
  • default export به‌جای named export: export default helper ولی import { helper }.
  • مسیر اشتباه: فایل وجود ندارد یا نام اشتباه است.
  • Circular imports: دو ماژول که به هم import می‌زنند.

راه‌حل: بررسی مستندات و استفاده‌ی درست از import/export:

// در utils.js
export function helper() { /* ... */ }
export default function main() { /* ... */ }

// در app.js
import main, { helper } from "./utils.js";
main();
helper();

مبانی ماژول‌ها در آموزش ES6 در جاوااسکریپت آمده است.

TypeError در event handlerها

هفتمین منبع، مربوط به event handlerها است. الگوی کلاسیک مشکل this:

class Counter {
    constructor() {
        this.count = 0;
    }
    
    increment() {
        this.count++;
    }
}

const counter = new Counter();
document.querySelector("#inc").addEventListener("click", counter.increment);
// Uncaught TypeError: Cannot read properties of undefined (reading "count")

در این مثال، this درون increment به undefined اشاره می‌کند چون متد به‌عنوان callback پاس داده شده. راه‌حل‌ها:

راه اول: bind

document.querySelector("#inc").addEventListener("click", counter.increment.bind(counter));

راه دوم: arrow function

document.querySelector("#inc").addEventListener("click", () => counter.increment());

راه سوم: کلاس فیلد با arrow function

class Counter {
    count = 0;
    increment = () => {
        this.count++;
    };
}

مبانی event و this در ایونت‌ها در جاوااسکریپت و شی‌گرایی در جاوااسکریپت آمده است.

TypeError در React، Vue و Angular

هشتمین منبع، مربوط به فریم‌ورک‌های مدرن است. در هر فریم‌ورک، این خطا از منابع خاص خود می‌آید:

در React

function UserProfile({ user }) {
    return <div>{user.name}</div>;
    // Uncaught TypeError اگر user undefined باشد
}

// راه‌حل
function UserProfile({ user }) {
    if (!user) return <div>Loading...</div>;
    return <div>{user.name}</div>;
}

در React، خطاهای رندر اگر توسط ErrorBoundary گرفته نشوند، کل درخت component را crash می‌کنند. راه‌حل: تعریف ErrorBoundary:

class ErrorBoundary extends React.Component {
    state = { hasError: false };
    static getDerivedStateFromError() {
        return { hasError: true };
    }
    render() {
        if (this.state.hasError) return <h1>Something went wrong</h1>;
        return this.props.children;
    }
}

در Vue

<template>
    <div>{{ user.name }}</div>
</template>

<script>
export default {
    props: {
        user: {
            type: Object,
            required: true,
            default: () => ({ name: "Guest" }),
        },
    },
};
</script>

در Angular

@Component({
    template: `<div *ngIf="user">{{ user.name }}</div>`
})
export class UserComponent {
    @Input() user: User | null = null;
}

هر فریم‌ورک، الگوهای خاص خود را برای مدیریت این خطا دارد. مبانی React در React از صفر آمده است.

روش تشخیص اصولی در پنج گام

در تجربه‌ی من، تشخیص Uncaught TypeError در چند دقیقه انجام می‌شود، اگر روش سیستماتیک داشته باشید:

گام اول: خواندن دقیق پیام خطا. پیام TypeError دقیقاً می‌گوید کدام property و کدام مقدار. این دو داده، جهت جستجو را تعیین می‌کند.

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

گام سوم: Pause on exceptions. در Chrome DevTools، تب Sources، گزینه‌ی Pause on exceptions را فعال کنید. این کار باعث می‌شود اجرای کد در لحظه‌ی وقوع خطا متوقف شود و بتوانید متغیرها را بررسی کنید:

console.log("Value:", value);
console.log("Type:", typeof value);
console.log("Is array:", Array.isArray(value));
value.map(x => x);

گام چهارم: بررسی مسیر داده. مسیر داده را از منبع تا نقطه‌ی خطا ردیابی کنید. چرا مقدار undefined شده؟ از API آمده؟ از storage؟ از state؟ ابزارهایی مثل Network tab در DevTools کمک‌کننده است.

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

ابزارهای تشخیص:

  • console.log و console.trace: چاپ اطلاعات و stack trace.
  • Chrome DevTools: تب Sources، Pause on exceptions.
  • Firefox Developer Tools: با debugger.
  • VS Code Debugger: با breakpoint و watch expressions.
  • ESLint: قواعد no-undef و no-implicit-globals.

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

راهبردهای رفع اصولی

بعد از تشخیص، نوبت به رفع می‌رسد. راهبردهای رفع، بر اساس نوع خطا متفاوت است:

راهبرد اول: بررسی صریح

if (value !== undefined && value !== null) {
    value.method();
} else {
    console.warn("Value is not defined");
}

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

راهبرد دوم: try/catch هدفمند

try {
    const result = riskyOperation();
} catch (error) {
    if (error instanceof TypeError) {
        console.error("Type error:", error.message);
    } else {
        throw error;  // سایر خطاها را رد کن
    }
}

نکته‌ی ظریف: همیشه استثنای خاص را catch کنید و سایر خطاها را دوباره پرتاب کنید.

راهبرد سوم: اعتبارسنجی ورودی

function validateUserInput(data) {
    if (!data || typeof data !== "object") {
        throw new TypeError("Expected object, got " + typeof data);
    }
    if (typeof data.name !== "string") {
        throw new TypeError("Expected name to be string");
    }
    return data;
}

راهبرد چهارم: بازنویسی منطق

در بعضی موارد، TypeError نشانه‌ی مشکل معماری است. مثلاً مدیریت state به‌جای مقادیر همزمان، یا استفاده از کلاس‌ها به‌جای اشیاء ناشناس. بازنویسی منطق، پایدارترین راه‌حل است.

راهبرد پنجم: استفاده از TypeScript

TypeScript با strict mode، بخش بزرگی از TypeErrorها را در زمان compile تشخیص می‌دهد. این رویکرد در پروژه‌های جدی، استاندارد است.

Optional Chaining و Nullish Coalescing

در ECMAScript 2020، دو اپراتور معرفی شد که بخش بزرگی از Uncaught TypeErrorها را حذف می‌کند:

Optional Chaining (?.)

const name = user?.name;
const city = user?.address?.city;
const first = arr?.[0];
const result = func?.();

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

Nullish Coalescing (??)

const name = user?.name ?? "Guest";
const count = data?.count ?? 0;

// تفاوت با ||
const value1 = 0 || "default";  // "default"
const value2 = 0 ?? "default";  // 0

ترکیب این دو اپراتور، کدی می‌سازد که تقریباً هیچ‌وقت Uncaught TypeError نمی‌دهد:

const city = user?.profile?.address?.city ?? "Unknown";

نکته‌ی مهم: Optional Chaining در تمام مرورگرهای مدرن (Chrome 80+، Firefox 74+، Safari 13.1+) پشتیبانی می‌شود. برای مرورگرهای قدیمی‌تر، از Babel یا TypeScript برای transpile استفاده کنید. جزئیات کامل در آموزش ES6 در جاوااسکریپت آمده است.

مدیریت مرکزی خطاها

در پروژه‌های بزرگ، مدیریت پراکنده‌ی try/catch کافی نیست. بهترین رویکرد، داشتن یک لایه‌ی مرکزی مدیریت خطا است:

window.addEventListener("error", (event) => {
    // خطاهای همزمان و خطاهای داخل event handlerها
    console.error("Global error:", event.error);
    reportToMonitoring(event.error);
});

window.addEventListener("unhandledrejection", (event) => {
    // خطاهای Promise مدیریت‌نشده
    console.error("Unhandled rejection:", event.reason);
    reportToMonitoring(event.reason);
});

function reportToMonitoring(error) {
    if (window.Sentry) {
        window.Sentry.captureException(error);
    }
}

این رویکرد، در پروژه‌های production، امکان پایش و تشخیص سریع خطاها را فراهم می‌کند. الگوهای کامل در مدیریت خطا در جاوااسکریپت آمده است.

TypeScript و جلوگیری در زمان compile

TypeScript با type system قدرتمند، بخش بزرگی از Uncaught TypeErrorها را قبل از اجرا تشخیص می‌دهد. مهم‌ترین ویژگی‌ها:

یک: strict mode. فعال‌سازی کامل strict: true در tsconfig.json:

{
    "compilerOptions": {
        "strict": true,
        "strictNullChecks": true,
        "noImplicitAny": true,
        "strictFunctionTypes": true
    }
}

دو: Union Types. تعریف مقادیر ممکن:

function getUser(id: number): User | null {
    const user = db.findUser(id);
    return user ?? null;
}

const user = getUser(5);
console.log(user.name);
// Error: Object is possibly null

// راه‌حل
if (user) {
    console.log(user.name);
}

سه: Type Guards. تعریف توابعی که نوع را تأیید می‌کنند:

function isValidUser(value: unknown): value is User {
    return (
        typeof value === "object" &&
        value !== null &&
        "name" in value &&
        typeof (value as User).name === "string"
    );
}

const data: unknown = await response.json();
if (isValidUser(data)) {
    console.log(data.name);  // TypeScript می‌داند data از نوع User است
}

در پروژه‌های مدرن، TypeScript با strict mode، بخش بزرگی از TypeErrorها را قبل از deployment کشف می‌کند. مبانی تفاوت TypeScript با JavaScript در تفاوت تایپ اسکریپت و جاوااسکریپت و آموزش تایپ اسکریپت از صفر آمده است.

Uncaught TypeError در محیط production

در محیط production، Uncaught TypeError ابعاد جدی‌تری دارد:

قطع سرویس

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

خطاهای آبشاری

وقتی یک Uncaught TypeError رخ می‌دهد و اجرای اسکریپت متوقف می‌شود، کدهای بعدی اجرا نمی‌شوند. این پدیده که به آن error cascade گفته می‌شود، باعث می‌شود یک خطای کوچک، به خطاهای بزرگ‌تر تبدیل شود. در داشبوردهای تحلیلی، این خطا می‌تواند منجر به رندر ناقص کل صفحه شود.

نشت اطلاعات

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

پایش و آلارم‌دهی

ابزارهایی مثل Sentry، Rollbar، و LogRocket، این خطاها را جمع‌بندی می‌کنند. نکته: بخش بزرگی از TypeErrorها از داده‌ی کاربر می‌آید و ممکن است بیهوده لاگ را پر کنند. راه‌حل: خطاهای مربوط به داده‌ی کاربر را از خطاهای برنامه جدا کنید.

پیشگیری با تست

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

  • Unit tests: تست توابع با ورودی‌های مختلف.
  • Integration tests: تست جریان کامل با داده‌های مرزی.
  • E2E tests: تست برنامه در مرورگر واقعی.
  • Error monitoring: پایش خطاها در production.

مبانی تست در مباحث JavaScript آمده است.

اشتباهات رایج در برخورد با این خطا

در طول سال‌ها، الگوهای تکراری از اشتباهات دیده‌ام که هر کدام می‌تواند پروژه را به چالش بکشد:

اشتباه اول: catch کردن عام

استفاده از catch (error) بدون بررسی نوع، می‌تواند ReferenceError یا SyntaxError را هم پنهان کند. راه‌حل: همیشه استثنای خاص را catch کنید:

try {
    riskyOperation();
} catch (error) {
    if (error instanceof TypeError) {
        handleTypeError(error);
    } else {
        throw error;
    }
}

اشتباه دوم: نادیده گرفتن stack trace

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

اشتباه سوم: فرض داده‌ی async آماده

فرض اینکه داده‌ی async قبل از render آماده است، در کوتاه‌مدت کار می‌کند ولی در بلندمدت به فاجعه می‌انجامد. راه‌حل: حالت loading صریح داشته باشید:

let data = null;

async function loadData() {
    data = await fetch("/api/data").then(r => r.json());
}

function render() {
    if (!data) return "Loading...";
    return data.name;
}

اشتباه چهارم: نبود Error Boundary در React

در React، اگر خطایی در component رخ دهد و error boundary نداشته باشید، کل برنامه crash می‌کند. راه‌حل: تعریف Error Boundary در سطح بالای درخت component.

اشتباه پنجم: نبود TypeScript

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

اشتباه ششم: نبود تست برای ورودی‌های نامعتبر

اگر تست‌ها فقط برای ورودی‌های معتبر نوشته شوند، TypeErrorها در production کشف می‌شوند. راه‌حل: برای هر تابع، تست‌های مرزی بنویسید.

اشتباه هفتم: نادیده گرفتن sourcemap در production

اگر sourcemap در production فعال نباشد، پیام خطا مبهم است و تشخیص را سخت می‌کند. راه‌حل: sourcemap را در production فعال کنید و آن را به ابزارهای پایش (مثل Sentry) وصل کنید.

Uncaught TypeError یک پیام از نوع است، نه از کد. JavaScript می‌گوید این عملیات روی این مقدار قابل انجام نیست. راه‌حل، بررسی نوع است نه سرکوب خطا.

پرسش‌های پرتکرار درباره خطای Uncaught TypeError

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

تفاوت Uncaught TypeError و TypeError چیست؟

TypeError نام کلاس خطاست. Uncaught TypeError همان خطاست ولی در هیچ try/catch گرفته نشده. اگر خطا در try/catch گرفته شود، دیگر Uncaught نیست و فقط به‌عنوان TypeError در بلوک catch دیده می‌شود.

چرا خطای Uncaught TypeError باعث توقف کل اسکریپت می‌شود؟

چون این خطا از نوع Error است و اگر مدیریت نشود، مرورگر اجرای اسکریپت را متوقف می‌کند. این یعنی تمام کد پس از خطا، اجرا نمی‌شود. برای جلوگیری از این رفتار، خطا را در try/catch بگیرید یا از Optional Chaining استفاده کنید.

چگونه از این خطا در کد asynchronous جلوگیری کنم؟

دو رویکرد: اول، تمام awaitها را در try/catch قرار دهید:

try {
    const response = await fetch("/api/user");
    const data = await response.json();
} catch (error) {
    console.error(error);
}

دوم، برای Promiseهای مدیریت‌نشده، از window.addEventListener("unhandledrejection", ...) استفاده کنید.

آیا Optional Chaining همیشه راه‌حل است؟

نه. Optional Chaining خطا را پنهان می‌کند ولی ریشه را حل نمی‌کند. اگر مقادیر undefined به‌طور غیرمنتظره ظاهر شوند، ممکن است منبع یک باگ بزرگ‌تر باشد. Optional Chaining را برای مواردی که undefined یک سناریوی طبیعی است (مثل داده‌ی اختیاری) استفاده کنید، نه برای پنهان کردن باگ.

چرا خطای Uncaught TypeError در production بیشتر دیده می‌شود؟

چون در محیط development، داده‌های تست معمولاً معتبر هستند. در production، داده‌ی واقعی می‌تواند شامل مقادیر undefined، null، یا فرمت غیرمنتظره باشد. راه‌حل: تست با داده‌های مرزی و استفاده از TypeScript.

آیا TypeScript می‌تواند از Uncaught TypeError جلوگیری کند؟

TypeScript با strict mode، بخش بزرگی از این خطاها را قبل از اجرا تشخیص می‌دهد. ولی TypeScript نمی‌تواند همه‌ی موارد را کشف کند (مخصوصاً در مرزهای runtime با داده‌ی خارجی). ترکیب TypeScript با اعتبارسنجی runtime، بهترین رویکرد است.

تفاوت Uncaught TypeError و Uncaught ReferenceError چیست؟

TypeError وقتی رخ می‌دهد که عملیات با نوع مقدار سازگار نیست. ReferenceError وقتی رخ می‌دهد که متغیر در هیچ scope وجود ندارد. تفاوت: TypeError از نوع می‌آید، ReferenceError از وجود. جزئیات کامل در خطای ReferenceError در جاوااسکریپت آمده است.

چگونه در React، از Uncaught TypeError جلوگیری کنم؟

سه رویکرد: اول، بررسی صریح props و state قبل از استفاده. دوم، استفاده از default props یا مقادیر پیش‌فرض. سوم، تعریف Error Boundary در سطح بالای درخت component. مبانی کامل در React از صفر آمده است.

آیا Uncaught TypeError روی performance تأثیر دارد؟

خود خطا در لحظه‌ی وقوع رخ می‌دهد. اگر مدیریت شود، از نظر performance هزینه‌ی ناچیزی دارد. اگر مدیریت نشود و اجرای اسکریپت متوقف شود، performance تحت تأثیر است چون بخشی از صفحه رندر نمی‌شود. در React، خطاهای رندر می‌توانند باعث re-render مکرر شوند که performance را تحت تأثیر قرار می‌دهد.

چگونه در Vue، از این خطا جلوگیری کنم؟

در Vue، از prop validation استفاده کنید:

props: {
    user: {
        type: Object,
        required: true,
        default: () => ({ name: "Guest" }),
    },
}

همچنین، از v-if برای conditional rendering استفاده کنید.

چرا خطا در console مرورگر با پیام متفاوتی نمایش داده می‌شود؟

هر مرورگر پیام متفاوتی تولید می‌کند. در Chrome و Node.js (موتور V8)، پیام‌ها دقیق‌تر هستند. در Firefox (SpiderMonkey) و Safari (JavaScriptCore)، پیام‌ها متفاوت است. ولی مفهوم در همه یکسان است و راه‌حل مشابه. برای دیباگ بین مرورگری، از ابزارهایی مثل BrowserStack یا Sauce Labs استفاده کنید.

چگونه از این خطا در Angular جلوگیری کنم؟

Angular با strict mode در TypeScript، بخش بزرگی از این خطاها را قبل از اجرا تشخیص می‌دهد. همچنین، از *ngIf و async pipe برای conditional rendering استفاده کنید:

<div *ngIf="user$ | async as user">
    {{ user.name }}
</div>

چگونه از این خطا در event handlerها جلوگیری کنم؟

سه رویکرد: اول، استفاده از arrow function برای حفظ this:

element.addEventListener("click", (e) => this.handleClick(e));

دوم، استفاده از bind:

element.addEventListener("click", this.handleClick.bind(this));

سوم، تعریف متد به‌عنوان arrow function در کلاس:

class MyClass {
    handleClick = (e) => { /* ... */ };
}

چرا خطا در import ماژول‌ها این‌قدر شایع است؟

چون ES Modules در JavaScript استاندارد نسبتاً جدیدی است و در پروژه‌های قدیمی ممکن است با CommonJS یا AMD ترکیب شود. همچنین، ابزارهای build مثل Webpack و Vite ممکن است رفتار متفاوتی در resolve ماژول‌ها داشته باشند. راه‌حل: از named export یا default export به‌طور یکنواخت استفاده کنید و مسیرها را دقیق بنویسید.

چگونه در Node.js، این خطا را مدیریت کنم؟

در Node.js، از process.on("uncaughtException", ...) برای گرفتن خطاهای مدیریت‌نشده استفاده کنید:

process.on("uncaughtException", (error) => {
    console.error("Uncaught:", error);
    process.exit(1);  // بسیار مهم
});

نکته‌ی حیاتی: پس از یک uncaughtException، وضعیت برنامه نامشخص است، بنابراین معمولاً بهتر است process با کد خطا خارج شود تا ابزارهایی مثل PM2 یا Kubernetes آن را restart کنند.

آیا می‌توانم از instanceof برای تشخیص TypeError استفاده کنم؟

بله:

try {
    riskyOperation();
} catch (error) {
    if (error instanceof TypeError) {
        // مدیریت خطای TypeError
    } else if (error instanceof ReferenceError) {
        // مدیریت ReferenceError
    } else {
        throw error;
    }
}

این رویکرد، در پروژه‌هایی که خطاهای متفاوتی دارند، مفید است.

تفاوت Uncaught TypeError و Uncaught Promise Rejection چیست؟

Uncaught TypeError یک خطای همزمان است که در هیچ try/catch گرفته نشده. Uncaught Promise Rejection زمانی رخ می‌دهد که یک Promise رد شود و هیچ .catch() یا try/catch برای آن وجود نداشته باشد. راه‌حل دومی با window.addEventListener("unhandledrejection", ...) است. جزئیات کامل در Promise در جاوااسکریپت آمده است.

آیا نصب ESLint می‌تواند از این خطا جلوگیری کند؟

ESLint با قواعد مناسب، بخش بزرگی از این خطاها را در زمان development کشف می‌کند. مهم‌ترین قواعد: no-undef (متغیر تعریف‌نشده)، no-implicit-globals (global ناخواسته)، و no-unused-vars. برای پروژه‌های TypeScript، از @typescript-eslint استفاده کنید.

آیا Uncaught TypeError می‌تواند از یک کتابخانه‌ی شخص ثالث بیاید؟

بله. این اتفاق زمانی می‌افتد که کتابخانه با نسخه‌ی JavaScript شما سازگار نیست یا API آن تغییر کرده. راه‌حل: changelog کتابخانه را بررسی کنید، نسخه‌ی نصب‌شده را با مستندات تطبیق دهید، و در صورت نیاز به نسخه‌ی سازگار بازگردید.

چگونه در SSR (Server-Side Rendering)، این خطا را مدیریت کنم؟

در SSR (Next.js، Nuxt.js)، کدهایی که به window، document، یا localStorage دسترسی دارند، در سرور undefined هستند و TypeError می‌دهند. راه‌حل: بررسی typeof window !== "undefined" یا استفاده از useEffect (در React) و onMounted (در Vue).

چگونه Uncaught TypeError را در Jest تست کنم؟

با toThrow:

test("throws TypeError on invalid input", () => {
    expect(() => {
        const user = undefined;
        user.name;
    }).toThrow(TypeError);
});

آیا Error Boundary در React همه‌ی خطاها را می‌گیرد؟

نه. Error Boundary خطاهای داخل رندر، lifecycle، و constructor را می‌گیرد، ولی خطاهای داخل event handlerها، setTimeout، و کد asynchronous را نمی‌گیرد. برای این موارد، از try/catch در سطح خود کد استفاده کنید.

چرا خطا در بعضی مرورگرها نمایش داده می‌شود ولی در بقیه نه؟

هر مرورگر مدیریت خطای متفاوتی دارد. Chrome و Firefox پیام‌های دقیق‌تری نمایش می‌دهند. Safari ممکن است پیام کوتاه‌تری بدهد. برای دیباگ بین مرورگری، از window.onerror استفاده کنید و پیام‌ها را در سرور لاگ کنید.

آنچه از سال‌ها کار با Uncaught TypeError در JavaScript آموختم

اگر بخواهم چکیده‌ی این سال‌ها را در چند جمله بگویم، سه اصل عملی دارم:

یک: Uncaught TypeError یک سیگنال از فرض‌های نادرست است، نه از کد بد. هر بار که این خطا می‌بینید، به‌جای سرزنش کد، فرض‌های خود را بررسی کنید. چرا فرض کردید این مقدار آرایه است؟ چرا فرض کردید این شیء وجود دارد؟ پاسخ این سؤالات، شما را به ریشه می‌رساند.

دو: TypeScript، سرمایه‌گذاری بلندمدت است. در پروژه‌های جدی، TypeScript با strict mode، بخش بزرگی از این خطاها را قبل از اجرا تشخیص می‌دهد. سرمایه‌گذاری اولیه، در بلندمدت چند برابر برمی‌گردد. اگر روی پروژه‌ی جدید کار می‌کنید، از همان روز اول TypeScript را فعال کنید.

سه: لایه‌ی مدیریت خطا، بیمه‌ی production است. تعریف یک لایه‌ی مرکزی مدیریت خطا با window.addEventListener("error", ...) و window.addEventListener("unhandledrejection", ...)، امکان پایش و تشخیص سریع را فراهم می‌کند. اتصال این لایه به ابزارهای مثل Sentry، تفاوت جدی در زمان پاسخ به خطاها ایجاد می‌کند.

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

هدف این مقاله، تمام‌کردن همه‌ی سناریوهای ممکن نبود. هدف، دادن یک چارچوب ذهنی برای تشخیص، پیشگیری و رفع این خطا بود. وقتی این چارچوب را درونی کنید، برخورد با Uncaught TypeError از یک واکنش اضطراری به یک فرآیند منظم تبدیل می‌شود. و این، همان تفاوتی است که بین توسعه‌دهنده‌ی معمولی و توسعه‌دهنده‌ای که به کدش اعتماد دارد، وجود دارد.

اگر Uncaught TypeError در پروژه‌ی شما به شکلی ظاهر شده که با الگوهای این مقاله حل نشده، برای من جالب است بدانم کدام سناریو بود. تجربه‌ی خودتان را در دیدگاه‌ها بنویسید؛ به‌ویژه اگر راه‌حلی پیدا کرده‌اید که هنوز در این مقاله نیست. 🐛