یک بار در یک فروشگاه اینترنتی، فرآیند ذخیره سبد خرید روی بعضی کاربران به‌کلی از کار افتاد و در کنسول، یک پیام تک‌خطی ظاهر شد: Cannot set property 'items' of undefined. مشکل نه از منطق کد بود و نه از دیتابیس؛ کافی بود شرطی که سبد خرید را می‌ساخت قبل از مقداردهی اجرا نشود و همه چیز بشکند. از آن روز، هر بار که این خطا را می‌بینم، پیش از هر چیز سراغ ترتیب مقداردهی می‌روم.

خطای Cannot set property of undefined دقیقاً چیست؟

خطای Cannot set property 'x' of undefined یکی از پیام‌های استاندارد موتور جاوااسکریپت است که زمانی ظاهر می‌شود که کد شما تلاش می‌کند پراپرتی‌ای را روی یک مقدار undefined بنویسد. جنس این خطا از خانواده TypeError است؛ یعنی نه یک خطای نگارشی، نه یک خطای محدوده، بلکه یک خطای نوع. موتور جاوااسکریپت در این لحظه به شما می‌گوید: «این مقدار، ظرفِ پراپرتی نیست؛ نمی‌توانم چیزی رویش بنویسم.»

این خطا در ECMAScript به‌عنوان بخشی از قواعد داخلی دسترسی به پراپرتی تعریف شده است. هر شیء در جاوااسکریپت یک ساختار داخلی به‌نام [[Set]] دارد که مشخص می‌کند وقتی می‌خواهید روی پراپرتی‌ای مقدار بگذارید، چه اتفاقی می‌افتد. اما مقادیر اولیه (primitive) مثل undefined و null اصلاً شیء نیستند؛ به همین دلیل، عملیات نوشتن روی آن‌ها از پایه بی‌معناست و موتور در همان لحظه خطا می‌دهد.

هر مقدار در جاوااسکریپت، قواعد مخصوص خودش را دارد؛ و undefined از پایه قواعد نوشتن را نمی‌پذیرد.

نکته مهمی که در بسیاری از پرونده‌ها به کارم آمده این است که پیام خطا همیشه شکل دقیق Cannot set property 'x' of undefined ندارد. بسته به موتور، ممکن است شکل‌های زیر را ببینید:

Cannot set property 'x' of undefined
Cannot set properties of undefined (setting 'x')
Cannot set property 'x' of null
Cannot set properties of null (setting 'x')

سه مورد اول و دوم، تفاوت نگارشی بین موتورهای قدیمی و جدید V8 است؛ ولی محتوای همه‌شان یکی است: تلاش برای نوشتن روی مقدار undefined یا null. به همین دلیل، هیچ‌وقت روی متن دقیق پیام تکیه نکنید؛ روی instanceof TypeError و بررسی message به‌شکل الگومحور تکیه کنید.

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

تفاوت undefined و null در سمت نوشتن

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

در سمت نوشتن، هم undefined و هم null خطای TypeError می‌دهند. تفاوت پیام در نام مقدار است؛ ولی از منظر برنامه‌نویسی، هیچ‌کدام قابل نوشتن نیستند. این ویژگی، نتیجه طراحی عامدانه زبان است؛ اگر نوشتن روی undefined مجاز بود، کل رفتار شیءگرایی جاوااسکریپت به‌هم می‌ریخت.

نکته ظریف‌تر در سمت خواندن است. undefined.x و null.x هر دو خطای TypeError می‌دهند، ولی رفتارشان در مقایسه‌های ضمنی متفاوت است:

undefined == null   // true
undefined === null  // false

این تفاوت، در بعضی الگوهای بررسی مقادیر، تفاوت بین «کار می‌کند» و «باگ پنهان» است. برای مثال، اگر شرط شما value == null باشد، هم undefined و هم null را می‌گیرد؛ ولی اگر شرط value === null باشد، فقط null را می‌گیرد و undefined از دست شما می‌رود. در تجربه من، بخش قابل‌توجهی از خطاهای «نوشتن روی undefined» از همین جزئیات ناشی می‌شود: شرط، undefined را نگرفته و بعد، مقداردهی روی مقدار نامنتظره اجرا شده است.

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

در سمت نوشتن، undefined و null هر دو مانع‌اند؛ ولی در سمت شرط، undefined راه خودش را از null جدا می‌کند.

تفاوت با Cannot read property of undefined

یکی از سؤالاتی که در جلسات بازبینی کد زیاد می‌شنوم این است: تفاوت Cannot set property با Cannot read property چیست؟ پاسخ در ظاهر ساده است ولی در عمل، مهم: اولی مربوط به نوشتن است، دومی مربوط به خواندن. ولی همین تفاوت ظاهری، در تشخیص ریشه مشکل، اثر عمیقی می‌گذارد.

در خطای خواندن، کد شما فرض کرده چیزی هست و می‌خواهد آن را بخواند. در خطای نوشتن، کد شما فرض کرده چیزی آماده است و می‌خواهد رویش مقدار بگذارد. بافت این دو، معمولاً متفاوت است. خطای خواندن بیشتر در مسیرهای «دریافت داده» ظاهر می‌شود؛ ولی خطای نوشتن در مسیرهای «مقداردهی و راه‌اندازی» رخ می‌دهد.

به همین دلیل، مسیر تشخیص این دو خطا متفاوت است. در خطای خواندن، سؤال اصلی این است: «چرا این مقدار undefined شد؟». در خطای نوشتن، سؤال اصلی این است: «چرا این ظرف قبل از نوشتن ساخته نشد؟». اگر این تفکیک را در ذهن داشته باشید، سرعت تشخیص خطا چند برابر می‌شود.

در تجربه من، پرونده‌های Cannot set property در سه کلاس اصلی جای می‌گیرند: مقداردهی در سازنده (constructor) که قبل از فراخوانی سازنده اجرا شده؛ مقداردهی روی شیئی که از یک عملیات async برگشته ولی هنوز نتیجه نداده؛ و مقداردهی روی شیئی که از یک منبع بیرونی گرفته شده و ساختارش با انتظار کد ما هم‌خوان نیست. اگر با این سه کلاس آشنا باشید، بخش بزرگی از پرونده‌های این خطا را می‌توانید سریع تحلیل کنید.

در کنار این تفکیک، لازم است بدانید که هر دو خطا در بافت async می‌توانند سریع‌تر و پیچیده‌تر ظاهر شوند. برای درک دقیق‌تر این الگو در زنجیره‌های ناهمگام، مرور «Promise در جاوااسکریپت» و «async و await در جاوااسکریپت» می‌تواند دید دقیق‌تری به شما بدهد؛ چون در این بافت، ترتیب اجرا و مقداردهی، نقش کلیدی بازی می‌کند.

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

جایگاه این خطا در خانواده TypeError

خطای Cannot set property of undefined عضوی از خانواده بزرگ TypeError است. برای اینکه در پروژه‌های چندخطایی بتوانید سریع تشخیص دهید کدام TypeError را پیش رو دارید، بد نیست اعضای پرتکرار این خانواده را کنار هم ببینید:

پیاممعناسطح خطا
Cannot read property 'x' of undefinedخواندن پراپرتی از undefinedسمت خواندن
Cannot set property 'x' of undefinedنوشتن پراپرتی روی undefinedسمت نوشتن
x is not a functionفراخوانی چیزی که تابع نیستسمت فراخوانی
Cannot convert undefined or null to objectتبدیل اجباری مقادیر پوچسمت تبدیل
Assignment to constant variableتغییر مقدار ثابتسمت انتساب

این جدول، در جلسات دیباگ بسیار به کارم آمده؛ چون وقتی پیام خطا کمی متفاوت از چیزی است که در مستندات دیده‌اید، سریع می‌توانید بفهمید که کدام دسته از مسئله را پیش رو دارید. یکی از پرتکرارترین موارد اشتباه، مخلوط کردن Cannot set property با x is not a function است؛ زیرا هر دو می‌توانند از یک ریشه بیایند ولی راه‌حل‌شان متفاوت است. برای درک دقیق‌تر دومی، مرور «خطای is not a function در جاوااسکریپت» می‌تواند کمک کند.

یک نکته فنی که در بازبینی کد زیاد می‌بینم: بعضی توسعه‌دهندگان برای «رفع سریع» خطای نوشتن، از تبدیل نوع اجباری استفاده می‌کنند؛ یعنی با Object(value) یا استفاده از spread، مقدار undefined را به یک شیء خالی تبدیل می‌کنند. این کار فقط صورت مسئله را پاک می‌کند؛ چون شیء خالی از نظر برنامه‌ای، همان undefined است ولی حالا مقداردهی رویش اجرا می‌شود و برنامه در لایه بعدی با خطای نامرئی‌تری مواجه می‌شود.

شش سناریوی واقعی که این خطا را فعال می‌کنند

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

سناریو اول: مقداردهی قبل از ساخت شیء

شایع‌ترین حالت. کد شما فرض می‌کند شیء وجود دارد، در حالی که شرطی که آن را می‌ساخت هنوز اجرا نشده. مثال کلاسیک در سازنده‌های کلاس که با inherit کار می‌کنند دیده می‌شود؛ اگر کلاس فرزند قبل از فراخوانی super() بخواهد پراپرتی‌ای مقداردهی کند، موتور خطا می‌دهد. برای درک دقیق این الگو در بافت شیءگرایی، مرور «شی گرایی در جاوااسکریپت» می‌تواند مفید باشد.

class Base {
  constructor() { this.ready = true; }
}

class Child extends Base {
  constructor() {
    this.status = "init"; // TypeError
    super();
  }
}

سناریو دوم: مقدار برگشتی از یک تابع که در بعضی مسیرها undefined است

تابعی که در بعضی مسیرها شیء برمی‌گرداند و در بعضی مسیرها undefined، منبع بسیاری از این خطاها است. اگر کد مصرف‌کننده، بر اساس فرض «همیشه شیء است» نوشته شده باشد، در مسیر undefined خطا می‌دهد. این الگو، در توابعی که با شرط‌های تودرتو نوشته شده‌اند، زیاد دیده می‌شود:

function getConfig(key) {
  if (key === "a") return { value: 1 };
  // مسیرهای دیگر برمی‌گردند undefined
}

const cfg = getConfig("b");
cfg.value = 2; // TypeError

سناریو سوم: نتیجه فراخوانی async که هنوز آماده نیست

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

async function load() {
  return { data: [] };
}

const state = load();       // Promise
state.data = [1, 2, 3];     // TypeError

سناریو چهارم: مقداردهی روی پراپرتی تودرتو بدون مقداردهی میانی

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

const state = {};
state.user.profile.name = "Ali"; // TypeError: Cannot set property 'profile' of undefined

این الگو در پروژه‌های مدیریت وضعیت (state management) بسیار شایع است و راه‌حل استاندارد، مقداردهی میانی قبل از نوشتن انتهایی است.

سناریو پنجم: spread روی مقدار ناشناس و ادغام اشتباه

الگوی spread که با { ...defaults, ...userInput } کار می‌کند، اگر یکی از ورودی‌ها undefined باشد، خطا نمی‌دهد ولی نتیجه ممکن است قابل مقداردهی نباشد. اینجا دام ظریف‌تری وجود دارد: در بعضی نسخه‌های قدیمی موتورها یا با ترنسپایلرهای خاص، همین الگو می‌تواند به خطای نوشتن منتهی شود:

const merged = { ...undefined };
merged.x = 1; // در بعضی بافت‌ها رفتار نامنتظره

توصیه من این است که همیشه قبل از spread، مقدار ورودی را با || {} یا ?? {} تمیز کنید.

سناریو ششم: بازگشت نادرست در کتابخانه‌های reactive

در کتابخانه‌های reactive مثل Vue و MobX، اگر یک computed property یا watcher به‌اشتباه یک مقدار undefined برگرداند، کد مصرف‌کننده آن را به‌عنوان شیء می‌بیند و می‌خواهد رویش مقدار بگذارد. این خطا، در نگاه اول از کتابخانه به نظر می‌رسد، ولی ریشه در تعریف آن computed یا watcher دارد. اگر در پروژه‌ای با این کتابخانه‌ها کار می‌کنید، همیشه مقدار بازگشتی computed را با یک مقدار پیش‌فرض آغاز کنید.

در هر شش سناریو، یک نکته مشترک وجود دارد: کد شما روی یک مقدار undefined، مقداردهی انجام داده. یعنی فرض کرده چیزی وجود دارد، در حالی که هنوز ساخته نشده. اگر این فرض را در جای درست اصلاح کنید، خطا از پایه حذف می‌شود.

در همه سناریوها، ریشه یک چیز است: کد شما روی ظرفی نوشت که هنوز ساخته نشده بود.

چرا Optional Chaining در سمت نوشتن جواب نمی‌دهد؟

یکی از بزرگ‌ترین منابع سردرگمی برای توسعه‌دهندگانی که با optional chaining (?.) آشنا شده‌اند این است که همین عملگر برای نوشتن کار نمی‌کند. یعنی obj?.x در سمت خواندن خطا نمی‌دهد، ولی obj?.x = 1 در سمت نوشتن اصلاً قابل نوشتن نیست و اگر تلاش کنید، خطای نگارشی دریافت می‌کنید. این محدودیت، نتیجه مستقیم طراحی زبان است.

علت فنی این ممنوعیت، در معنای optional chaining است: این عملگر یعنی «اگر مقدار undefined بود، بی‌صدا هیچ کاری نکن.» در سمت خواندن این کاملاً بی‌ابهام است؛ در سمت نوشتن اما معنا ندارد. اگر obj?.x = 1 کار می‌کرد، موتور باید تصمیم می‌گرفت که وقتی obj undefined است، آیا مقداردهی را نادیده بگیرد یا خطا بدهد. چون هیچ‌کدام از این دو انتخاب، با منطق زبان سازگار نبود، طراحان زبان این نوشتار را از پایه منع کردند.

در تجربه من، این محدودیت به‌شکل غیرمستقیم باعث می‌شود تیم‌ها به سراغ راه‌حل‌های جایگزین بروند که خودشان منبع خطاهای جدید می‌شوند. یکی از پرتکرارترین الگوهای جایگزین، این است که تیم فنی از عملگر ??= استفاده می‌کند که در ECMAScript 2021 اضافه شد:

obj.x ??= 1;      // فقط اگر x undefined یا null است، مقداردهی می‌کند

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

obj ??= {};
obj.x ??= 1;

در بافت‌های پیچیده‌تر که مسیر تودرتو است، می‌توانید از یک helper ساده استفاده کنید که مسیر را قبل از نوشتن بسازد. برای درک عمیق‌تر قابلیت‌های مدرن زبان که این الگوها به آن‌ها وابسته‌اند، مرور «آموزش es6 در جاوااسکریپت» می‌تواند دید جامع‌تری به شما بدهد.

Optional chaining در سمت خواندن یعنی «اگر نبود، مهم نیست»؛ در سمت نوشتن، این جمله بی‌معناست و به همین دلیل، زبان آن را نمی‌پذیرد.

چطور خطای فعلی را در پروژه ایزوله کنیم؟

فرض کنید همین امروز یک خطای Cannot set property of undefined در محیط تولید ظاهر شده و می‌خواهید ریشه‌اش را پیدا کنید. روشی که در این نوع پرونده‌ها به کار می‌گیرم، چهار گام دارد و هر گام، یک شرط را در ذهن من حذف می‌کند.

گام اول: نگاه به نام پراپرتی در پیام خطا

اولین کاری که می‌کنم این است که نام پراپرتی‌ای که در پیام آمده را دقیق می‌خوانم. اگر نام پراپرتی، خاص باشد — مثلاً config یا response — می‌توانم به‌سرعت محل نوشتن را در کد پیدا کنم. اگر نام عمومی باشد — مثلاً x یا value — باید سراغ ابزارها بروم. این تفکیک ساده، در تجربه من نیمی از زمان دیباگ را کم می‌کند.

گام دوم: بررسی stack trace

stack trace این خطا، معمولاً به‌شکل دقیق محل نوشتن را نشان می‌دهد. ولی نکته ظریف این است که stack در خطاهای نوشتن غالباً کوتاه‌تر از خطاهای خواندن است؛ چون نوشتن، معمولاً در همان تابعی اتفاق می‌افتد که مقداردهی ناموفق بوده. اگر stack خیلی کوتاه و مبهم بود، احتمال دارد نوشتن درون یک callback یا درون یک promise شکل گرفته باشد. ابزارهای مرورگر در این مرحله کمک بزرگی هستند؛ فهرست کامل‌تر آن‌ها در «ابزارهای اشکال‌زدایی جاوااسکریپت» جمع شده است.

گام سوم: بازتولید و ثبت مقادیر میانی

سومین کاری که می‌کنم، بازتولید خطا در محیط توسعه و ثبت مقادیر میانی است. یک راه ساده برای این کار، این است که قبل از خط مقداردهی، همه متغیرهای مسیر را با console.log چاپ کنم. اگر مقدار میانی undefined بود، منبع واضح است. اگر مقدار میانی شیء موجودی بود ولی خطا باز هم رخ می‌داد، احتمال دارد مسئله در ساختمان شیء (shape) یا در Proxy باشد. روش گام‌به‌گام این عیب‌یابی را می‌توانید با الگوهای توضیح‌داده‌شده در «چگونه خطاهای جاوااسکریپت را در کنسول مرورگر پیدا کنیم» اجرا کنید.

گام چهارم: بررسی ترتیب اجرا

بسیاری از این خطاها، ریشه در ترتیب اجرا دارند. اگر کد شما در لایه‌ای بالاتر، قبل از مقداردهی، پراپرتی‌ای را می‌خواند یا می‌نویسد، ترتیب اجرا مقصر است. برای تشخیص این نوع، لازم است که اجرای برنامه را در مراحل اولیه متوقف کنید و مقدار متغیرها را در هر مرحله بررسی کنید. این روش، در پرونده‌های مبتنی بر state management بسیار مؤثر است.

یک تکنیک ساده اما مؤثر که در پروژه‌ها به کارم آمده: در نقطه‌ای که خطا رخ می‌دهد، پیش از خط مقداردهی، یک debugger; بگذارید. موتور در همان لحظه متوقف می‌شود و شما می‌توانید مقدار همه متغیرها را در آن نقطه بررسی کنید. این روش، سرعت تشخیص را به‌شکل چشمگیری بالا می‌برد.

در صورتی که خطا در بافت async رخ داده، بهتر است پیش از هر اقدامی، مطمئن شوید که await در همه مسیرها به‌درستی قرار گرفته. این نوع خطا در بافت async، بیشتر از هر چیز به فراموشی await مربوط می‌شود.

الگوهای رفع و پیشگیری در کد مدرن

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

الگوی اول: مقداردهی پیش از دسترسی

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

function ensure(obj, path) {
  const parts = path.split(".");
  let current = obj;
  for (let i = 0; i < parts.length; i++) {
    const key = parts[i];
    if (current[key] == null) {
      current[key] = {};
    }
    current = current[key];
  }
  return current;
}

const state = {};
ensure(state, "user.profile").name = "Ali";

این helper، در پروژه‌های با ساختار تودرتو بسیار مفید است؛ چون هم از خطا جلوگیری می‌کند و هم منطق مقداردهی را یک‌جا متمرکز می‌سازد.

الگوی دوم: استفاده از Object.assign با مقدار پیش‌فرض

در بافت‌هایی که می‌خواهید یک شیء ناقص را با مقدار پیش‌فرض ترکیب کنید، Object.assign ابزار استانداردی است:

function withDefaults(input, defaults) {
  return Object.assign({}, defaults, input || {});
}

این الگو، در کتابخانه‌های UI و در حالت‌های تنظیمات، بسیار به کار می‌آید.

الگوی سوم: Nullish coalescing assignment

در ECMAScript 2021، عملگر ??= اضافه شد که مقداردهی هوشمند را ممکن می‌کند. این عملگر، فقط وقتی مقدار undefined یا null است مقداردهی انجام می‌دهد:

state.user ??= {};
state.user.profile ??= {};
state.user.profile.name = "Ali";

این الگو، تمیزترین راه‌حل برای مقداردهی زنجیره‌ای است و در پروژه‌های مدرن توصیه می‌شود.

الگوی چهارم: الگوی Builder

در پروژه‌هایی که ساختار داده‌ها پیچیده است، الگوی Builder می‌تواند خطاها را از پایه حذف کند:

class StateBuilder {
  constructor() { this.state = {}; }
  withUser() { this.state.user = {}; return this; }
  withProfile() { this.state.user.profile = {}; return this; }
  withName(name) { this.state.user.profile.name = name; return this; }
  build() { return this.state; }
}

const state = new StateBuilder().withUser().withProfile().withName("Ali").build();

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

الگوی پنجم: اعتبارسنجی پیش از مقداردهی با Schema

در پروژه‌های جدی، استفاده از یک کتابخانه اعتبارسنجی مثل zod یا joi توصیه می‌شود؛ چون پیش از مقداردهی، ساختار ورودی را بررسی می‌کند و خطاهای نوع را زودتر می‌گیرد:

const schema = z.object({ user: z.object({}).default({}) });
const data = schema.parse(input);
data.user.name = "Ali";

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

حذف کامل این خطا از سیستم، نتیجه ترکیب چند الگوی طراحی است، نه یک توصیه تک‌خطی.

اشتباهات رایجی که این خطا را تشدید می‌کنند

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

  • استفاده از try/catch بدون درک ریشه. try/catch فقط جلوی سقوط را می‌گیرد؛ داده‌ای که قرار بوده نوشته شود، همچنان ناقص می‌ماند و در جای دیگری به باگ ظریف‌تری تبدیل می‌شود.
  • تبدیل undefined به {} بدون تفکیک. هر undefined به‌معنای «شیء خالی» نیست. در بعضی موارد، undefined به‌معنای «داده هنوز نیامده» و در موارد دیگر، «خطای بالادست». هر دو مورد، نیاز به درمان متفاوتی دارند.
  • نادیده گرفتن undefined در پارامترها. اگر تابع شما پارامتر با مقدار پیش‌فرض دارد، همیشه مطمئن شوید که مقدار پیش‌فرض، منطقی است. مقدار پیش‌فرض شیءِ خالی، گاهی خودش منبع خطاهای ظریف می‌شود.
  • نادیده گرفتن Optional Chaining در سمت نوشتن. چون این عملگر در نوشتن کار نمی‌کند، بعضی تیم‌ها به سراغ راه‌حل‌های پیچیده می‌روند. در واقع، بهترین راه‌حل، پیش از دسترسی، ظرف را بسازید.
  • مقداردهی روی prototype. گاهی به‌اشتباه پراپرتی‌ای روی prototype تنظیم می‌شود که خودش خطا می‌دهد. همیشه پیش از مقداردهی، بفهمید که این شیء، نمونه‌ای از چه چیزی است.
  • نادیده گرفتن تفاوت بین مقدار undefined و پراپرتی undefined. اگر پراپرتی‌ای روی شیء وجود نداشته باشد، مقدار آن undefined است ولی خود شیء موجود است. این تفاوت در تصمیم‌گیری نقش کلیدی دارد.

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

پیامدهای امنیتی مقداردهی روی مقادیر ناشناس

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

حمله prototype pollution

اگر مقدار ورودی از کاربر به‌طور مستقیم روی شیئی مقداردهی شود و ورودی شامل کلیدهای خاص مثل __proto__ باشد، ممکن است شیء اصلی آلوده شود. این الگو که به prototype pollution شناخته می‌شود، در بعضی کتابخانه‌های ادغام شیء دیده شده است. راه‌حل استاندارد، اعتبارسنجی کلیدهای ورودی و ممنوع کردن کلیدهای خاص است.

const forbidden = new Set(["__proto__", "constructor", "prototype"]);
function safeSet(target, key, value) {
  if (forbidden.has(key)) return;
  target[key] = value;
}

نشت اطلاعات از طریق خطاهای دقیق

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

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

ماتریس تست برای سناریوهای مقداردهی

چیزی که در پروژه‌های بالغ به‌شکل منظم دیده‌ام، تست‌های اختصاصی برای مقداردهی است. ماتریسی که در پروژه‌ها استفاده می‌کنم، این شکلی است:

سناریوورودیخروجی مورد انتظار
مقداردهی روی شیء خالی{}موفقیت
مقداردهی روی undefinedundefinedTypeError
مقداردهی روی nullnullTypeError
مقداردهی تودرتو بدون ساخت میانی{} بدون userTypeError
مقداردهی با helper مقداردهیمسیر تودرتوموفقیت
مقداردهی با ??=مسیر تودرتوموفقیت
مقداردهی با optional chaining—خطای نگارشی (نامعتبر)
مقداردهی پس از اعتبارسنجی schemaورودی نامعتبرخطای اعتبارسنجی

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

یک تذکر مهم: در تست‌های async، مطمئن شوید که هر تست، به‌شکل دقیق، خطا را در همان لایه‌ای که انتظار دارید دریافت می‌کند. بعضی از فریم‌ورک‌های تست، خطاها را در لایه‌ای بالاتر می‌گیرند و اگر شما به آن توجه نکنید، تست‌های موفق می‌سازید که در محیط واقعی شکست می‌خورند.

پرسش‌های پرتکرار درباره Cannot set property of undefined

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

تفاوت این خطا با Cannot read property of undefined چیست؟

اولی در سمت نوشتن رخ می‌دهد و دومی در سمت خواندن. بافت آن‌ها متفاوت است: خطای خواندن معمولاً در مسیرهای «دریافت داده» و خطای نوشتن در مسیرهای «مقداردهی و راه‌اندازی» ظاهر می‌شود. برای درک دقیق‌تر خطای خواندن، مرور «خطای Cannot read property of undefined» توصیه می‌شود.

چرا optional chaining در سمت نوشتن کار نمی‌کند؟

چون معنای آن در سمت نوشتن بی‌ابهام نیست. اگر obj?.x = 1 کار می‌کرد، موتور باید تصمیم می‌گرفت که وقتی obj undefined است، مقداردهی نادیده گرفته شود یا خطا بدهد. هیچ‌کدام از این دو انتخاب، با منطق زبان سازگار نبود؛ به همین دلیل، طراحان زبان این نوشتار را از پایه منع کردند.

آیا از ??= می‌توان به‌جای optional chaining استفاده کرد؟

فقط بخشی از کار را انجام می‌دهد. ??= در مقداردهی روی پراپرتی موجود کمک می‌کند، ولی اگر ظرف میانی undefined باشد، همچنان خطا می‌دهد. راه‌حل ترکیبی، مقداردهی ظرف میانی با ??= و سپس استفاده از آن برای پراپرتی نهایی است.

آیا تبدیل undefined به {} کار درستی است؟

همیشه نه. در بعضی موارد، undefined به‌معنای «داده هنوز نیامده» و در موارد دیگر، «خطای بالادست». تبدیل کورکورانه، اطلاعات را از بین می‌برد. همیشه بر اساس بافت تصمیم بگیرید.

آیا در TypeScript این خطا اتفاق می‌افتد؟

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

چطور بفهمم خطا از کد من است یا از کتابخانه؟

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

آیا مقداردهی روی primitiveها هم خطا می‌دهد؟

در حالت strict mode، مقداردهی روی primitiveها خطا می‌دهد. در حالت غیر strict، مقداردهی بی‌اثر است و خطا نمی‌دهد. تفاوت این دو رفتار، در پروژه‌های با ترنسپایلر متفاوت، می‌تواند منبع باگ‌های نامرئی باشد.

نگاه معمارانه: مقداردهی به‌عنوان یک تصمیم طراحی

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

لایه اول: مدل داده صریح

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

لایه دوم: مقداردهی متمرکز در مرزهای سیستم

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

لایه سوم: نوع‌های متمایز برای داده‌های ممکن‌العدول

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

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

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

یک عادت کوچک، یک کلاس خطای ازیادرفته

خطای Cannot set property of undefined در نگاه اول یک خطای کوچک به‌نظر می‌رسد، اما در عمل، آینه‌ای است که نشان می‌دهد مدل داده پروژه شما چقدر صریح و کنترل‌شده است. اگر این خطا در تولید ظاهر می‌شود، به احتمال زیاد جای دیگری از سیستم هم فرض‌های ضمنی درباره ساختار داده دارد. به همین دلیل، توصیه عملی من سه چیز است: اول، پیش از هر مقداردهی، از وجود ظرف مطمئن شوید؛ دوم، در مرزهای سیستم، یک لایه اعتبارسنجی متمرکز بگذارید؛ سوم، در تست‌های خود ماتریس سناریوهای مقداردهی را بگنجانید تا رفتار برنامه در برابر تغییرات ناخواسته، قابل پیش‌بینی بماند.

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