بار اول که این خطا را در یک پروژه جدی دیدم، در یک سرویس Node.js بود که بدون هیچ تغییری در کد، بعد از یک به‌روزرسانی وابستگی‌ها از کار افتاد. پیام ساده بود: res.json is not a function. مشکل نه از منطق بود و نه از syntax؛ کافی بود ترتیب ورودی‌های یک middleware جابه‌جا شود تا تابعی که انتظارش را داشتیم، در آن جایگاه وجود نداشته باشد. از آن روز، این خطا برای من به یک نشانه تبدیل شد: جایی در زنجیره فراخوانی، یک مقدار، هویت تابعی خودش را از دست داده است.

خطای is not a function دقیقاً چیست؟

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

نکته‌ای که در نگاه اول پنهان می‌ماند این است که این پیام بسته به موتور و نسخه، شکل‌های متفاوتی دارد. در موتورهای قدیمی‌تر V8، شکل رایج X is not a function بود؛ در نسخه‌های جدیدتر، پیام‌ها دقیق‌تر شده‌اند و گاهی به‌صورت X is not a function با جزئیات بیشتر نمایش داده می‌شوند. در موتور SpiderMonkey، شکل رایج X is not a function باقی مانده ولی جزئیات پیام ممکن است متفاوت باشد.

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

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

یکی از ظرایف مهم این است که در JavaScript، توابع خودشان شیء هستند و می‌توانند پراپرتی داشته باشند. یعنی چیزی که با پراپرتی قابل دسترسی است، لزوماً قابل فراخوانی نیست. همین ویژگی، منبع بسیاری از سردرگمی‌ها است. مثال معروف:

const arr = [1, 2, 3];
arr.length();        // TypeError: arr.length is not a function
arr.length;          // 3 (به‌درستی مقدار عددی برمی‌گرداند)

const str = "hello";
str.toUpperCase;     // تابع است
str.toUpperCase();   // "HELLO"
str.length();        // TypeError: str.length is not a function

همین چند خط ساده، ذات مسئله را نشان می‌دهد: در همه این مثال‌ها، مقدار وجود دارد ولی تابع نیست. یعنی خطای is not a function همیشه به‌معنای «وجود ندارد» نیست؛ گاهی به‌معنای «وجود دارد ولی جنس اشتباه است».

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

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

خطای is not a function عضوی از خانواده بزرگ TypeError است. برای اینکه در پروژه‌های چندخطایی بتوانید سریع تشخیص دهید کدام TypeError را پیش رو دارید، بد نیست اعضای پرتکرار این خانواده را کنار هم ببینید:

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

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

نکته ظریف دیگر این است که X is not a function می‌تواند در بعضی بافت‌ها با پیام‌های نزدیک به آن، مثل X is not a constructor یا Class constructor X cannot be invoked without 'new' اشتباه گرفته شود. تفاوت این‌ها در جنس مقدار است: دومی به کلاس اشاره دارد که تنها با new قابل استفاده است، ولی اولی به مقداری که اصلاً تابع نیست. در تجربه من، این تمایز در پروژه‌های مبتنی بر OOP بسیار مهم است؛ چون درمان این دو خطا، کاملاً متفاوت است.

تفاوت با خطای undefined و Cannot read property

یکی از سؤالاتی که در جلسات بازبینی کد زیاد می‌شنوم این است: تفاوت is not a function با Cannot read property of undefined چیست؟ پاسخ در ظاهر ساده است ولی در عمل، مهم: اولی به جنس مقدار مربوط می‌شود، دومی به وجود مقدار.

در خطای Cannot read property، موتور به شما می‌گوید که یک مقدار پایه (مثلاً obj) وجود ندارد ولی شما می‌خواهید از رویش چیزی بخوانید. در خطای is not a function، مقدار پایه وجود دارد، ولی آنچه می‌خواهید فراخوانی کنید، در آن مقدار جنس تابعی ندارد. بافت این دو خطا معمولاً متفاوت است: خطای اول بیشتر در مسیرهای «دسترسی به داده» رخ می‌دهد، خطای دوم در مسیرهای «فراخوانی رفتار».

در تجربه من، پرونده‌های is not a function در پنج کلاس اصلی جای می‌گیرند: فراخوانی روی مقادیر اولیه مثل undefined یا null؛ نام اشتباه در متد؛ مشکل در import و export؛ shadowing متغیر؛ و از دست رفتن this در متدهای جدا افتاده. اگر با این پنج کلاس آشنا باشید، بخش بزرگی از پرونده‌های این خطا را می‌توانید سریع تحلیل کنید.

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

شش ریشه واقعی این خطا در پروژه‌های حرفه‌ای

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

ریشه اول: نام اشتباه در متد یا پراپرتی

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

const arr = [1, 2, 3];
arr.forEach(el => console.log(el)); // درست
arr.foreach(el => console.log(el)); // TypeError: arr.foreach is not a function

در این مثال، تفاوت فقط یک حرف بزرگ و کوچک است؛ ولی موتور جاوااسکریپت به بزرگی و کوچکی حروف حساس است. این نوع خطا در کدهایی که با ویرایشگرهای بدون auto-complete نوشته می‌شوند، بسیار شایع است.

ریشه دوم: فراخوانی روی مقدار undefined یا null

در این حالت، مقدار پایه وجود ندارد و هرچه هم در پیام ببینید، مقصر واقعی همان مقدار پوچ است:

let config;
config.load();  // TypeError: Cannot read property 'load' of undefined

تفاوت ظریف: در این حالت، پیام می‌تواند از جنس Cannot read property باشد، چون موتور در مرحله اول نمی‌تواند پراپرتی را از روی مقدار بخواند. ولی اگر مقدار پایه چیزی غیر از undefined باشد که پراپرتی ندارد، پیام به‌شکل X is not a function ظاهر می‌شود. برای مرور دقیق‌تر رفتار undefined در این خانواده خطا، مطالعه «خطای undefined در جاوااسکریپت» توصیه می‌شود.

ریشه سوم: فراخوانی روی مقدار پوچ null

همانند حالت بالا، ولی با null. جالب است که در بعضی بافت‌ها، پیام خطا Cannot read properties of null (reading 'x') است و در بعضی بافت‌ها به‌شکل X is not a function ظاهر می‌شود. تفاوت این دو بسته به ساختار کد و نسخه موتور است؛ ولی درمان یکسان است: باید مقدار پایه را قبل از فراخوانی بررسی کنید.

ریشه چهارم: مقدار اشتباه در بازگشت تابع

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

function getHandler(type) {
  if (type === "click") return () => console.log("clicked");
  // مسیرهای دیگر برمی‌گردند undefined
}

const h = getHandler("hover");
h();  // TypeError: h is not a function

این الگو، در توابع کارخانه‌ای (factory functions) و در مسیرهای مدیریت رخداد (event handling) بسیار شایع است. راه‌حل استاندارد، بازگشت یک تابع پیش‌فرض در همه مسیرها است.

ریشه پنجم: پراپرتی به‌جای متد

در این حالت، پراپرتی مورد نظر وجود دارد ولی یک مقدار غیرتابعی است. مثال:

const obj = {
  name: "Ali",
  greet: "Hello"
};
obj.greet();  // TypeError: obj.greet is not a function

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

ریشه ششم: تغییر ساختار در زمان اجرا

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

class Service {}
const s = new Service();
s.init();  // TypeError: s.init is not a function
Service.prototype.init = function () { console.log("init"); };

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

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

دام import و named export در پروژه‌های مدرن

در پروژه‌های مبتنی بر ماژول (ES Modules یا CommonJS)، دام import یکی از رایج‌ترین منابع این خطا است. مسئله این است که وقتی شما یک ماژول را import می‌کنید، آن‌چه دریافت می‌کنید، لزوماً همان چیزی نیست که در سمت export نوشته شده است. سه حالت زیر، پرتکرارترین دام‌های این خانواده هستند.

حالت اول: نام اشتباه در named import

وقتی یک ماژول چیزی را با نام foo export می‌کند و شما آن را با نام bar import می‌کنید، در زمان بارگذاری خطا نمی‌گیرید؛ بلکه در زمان فراخوانی، با پیام bar is not a function مواجه می‌شوید. موتور در زمان import، فقط بررسی می‌کند که چیزی با آن نام وجود دارد یا نه، و چون در نسخه‌های جدیدتر زبان، این بررسی سخت‌گیرانه‌تر شده، پیام دقیق‌تری نمایش داده می‌شود؛ ولی در بعضی باندلرها، این خطا به‌شکل undefined is not a function ظاهر می‌شود.

حالت دوم: مخلوط کردن default و named import

یکی از اشتباهات رایج، import کردن یک default export با سینتکس named است:

// در ماژول مبدأ
export default function greet() {}
export function farewell() {}

// در ماژول مقصد
import { greet, farewell } from "./module.js";
// greet در اینجا undefined است چون default است
greet();  // TypeError: greet is not a function

راه‌حل، استفاده از سینتکس صحیح است: import greet, { farewell } from "./module.js". این نوع خطا در پروژه‌های TypeScript که با babel کامپایل می‌شوند، بسیار شایع است.

حالت سوم: دام circular dependency

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

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

در دام import، پیام خطا در زمان فراخوانی ظاهر می‌شود ولی ریشه در زمان بارگذاری نهفته است.

دام shadowing و بازنویسی متغیر

دام دیگری که در پروژه‌های بزرگ شایع است، shadowing یا «سایه‌انداختن» متغیر است. وقتی در یک اسکوپ داخلی، متغیری با همان نام متغیر اسکوپ بیرونی تعریف می‌شود، متغیر داخلی جایگزین می‌شود. اگر مقدار داخلی، به‌اشتباه یک مقدار غیرتابعی باشد، خطای is not a function رخ می‌دهد.

function processArray(arr) {
  const map = "some string";  // این متغیر نامش با متد map تداخل دارد
  return arr.map(x => x * 2);  // TypeError: map is not a function
}

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

یک نکته ظریف: در JavaScript، تفاوت بین var و let در این بافت بسیار مهم است. با var، متغیر در اسکوپ تابع سایه می‌اندازد و می‌تواند منبع خطاهای مبهم‌تری باشد. با let، متغیر فقط در بلوک خودش معتبر است و مدیریت آن آسان‌تر است. توصیه استاندارد، استفاده از let و const است؛ چون این دو، رفتار قابل پیش‌بینی‌تری دارند.

در پروژه‌هایی که از ابزارهای lint استفاده می‌کنند، معمولاً قاعده no-shadow فعال است و این خطاها را در زمان توسعه می‌گیرد. اگر در پروژه شما این قاعده فعال نیست، پیشنهاد می‌کنم آن را در همان ابتدای پروژه فعال کنید. برای درک دقیق‌تر این ابزارها و قواعد آن‌ها، مرور «ابزارهای اشکال‌زدایی جاوااسکریپت» می‌تواند مفید باشد.

در بافت ماژول‌های بزرگ، سایه‌انداختن می‌تواند از یک ماژول به ماژول دیگر هم شکل بگیرد. مثلاً اگر در یک ماژول، تابعی با نام map تعریف شده باشد و در ماژول دیگری، همان نام برای متغیر محلی استفاده شود، تداخل ممکن است رخ دهد. راه‌حل استاندارد، نام‌گذاری معنادار و منحصربه‌فرد برای متغیرها است.

دام متدهای آرایه و شبیه‌سازی‌های ناقص

یکی از رایج‌ترین سناریوهای خطای is not a function در پروژه‌های مدرن، فراخوانی متدهای آرایه روی مقادیری است که واقعاً آرایه نیستند، ولی شبیه آرایه رفتار می‌کنند. این الگو در سه بافت زیر بیشتر دیده می‌شود.

بافت اول: فراخوانی متد روی NodeList یا HTMLCollection

وقتی با document.querySelectorAll کار می‌کنید، نتیجه یک NodeList است، نه یک آرایه. این شیء، متدهای آرایه مثل map، filter و reduce ندارد و اگر سعی کنید آن‌ها را فراخوانی کنید، خطا رخ می‌دهد:

const nodes = document.querySelectorAll(".item");
nodes.map(n => n.textContent);  // TypeError: nodes.map is not a function

// راه‌حل:
Array.from(nodes).map(n => n.textContent);

در تجربه من، این الگو در پروژه‌هایی که تازه به JavaScript مدرن مهاجرت کرده‌اند، بسیار شایع است. حل سریع آن، استفاده از Array.from یا spread است. برای مرور کامل‌تر متدهای آرایه و تفاوت آن‌ها با شبه‌آرایه‌ها، مطالعه «آرایه‌ها در جاوااسکریپت» توصیه می‌شود.

بافت دوم: فراخوانی متد روی arguments

شیء arguments در توابع قدیمی، شبه‌آرایه است و متدهای آرایه ندارد. اگر در یک تابع با function کار می‌کنید و می‌خواهید روی arguments متد آرایه‌ای اعمال کنید، باید ابتدا آن را به آرایه تبدیل کنید:

function sum() {
  return arguments.reduce((a, b) => a + b);  // TypeError: arguments.reduce is not a function
}

// راه‌حل:
function sum() {
  return Array.from(arguments).reduce((a, b) => a + b);
}

در پروژه‌های مدرن، توصیه استاندارد استفاده از rest parameters است که از ES6 اضافه شد:

function sum(...nums) {
  return nums.reduce((a, b) => a + b);
}

بافت سوم: فراخوانی متد روی رشته

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

const s = "hello";
s.length;             // 5 (پراپرتی، نه متد)
s.map(c => c);        // TypeError: s.map is not a function

// راه‌حل:
[...s].map(c => c);   // آرایه‌ای از کاراکترها

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

در کنار این سه بافت، یک الگوی چهارم هم وجود دارد که در پروژه‌های reactive مثل Vue و MobX دیده می‌شود: بعضی از آرایه‌های reactive، متدهایشان به‌عنوان تابع قابل فراخوانی نیستند و باید با متدهای خاص کتابخانه کار کرد. این نوع خطا در نگاه اول از کتابخانه به نظر می‌رسد، ولی ریشه در تفاوت بین آرایه معمولی و آرایه reactive دارد.

دام this binding و از دست رفتن هویت متد

یکی از ظریف‌ترین منابع خطای is not a function، از دست رفتن this در متدهای جدا افتاده است. در JavaScript، مقدار this در زمان فراخوانی مشخص می‌شود، نه در زمان تعریف. اگر متدی را از شیء جدا کنید و به‌تنهایی فراخوانی کنید، this به مقدار دیگری اشاره می‌کند و در نتیجه، متد نمی‌تواند به پراپرتی‌های شیء اصلی دسترسی داشته باشد.

const obj = {
  name: "Ali",
  greet() {
    return "Hello " + this.name;
  }
};

const fn = obj.greet;
fn();  // this.name در اینجا undefined است یا به window اشاره می‌کند

در این مثال، خطا به‌شکل مستقیم is not a function ظاهر نمی‌شود، ولی اگر داخل متد، فراخوانی متد دیگری روی this وجود داشته باشد، همان خطا ظاهر می‌شود. راه‌حل استاندارد، استفاده از bind است:

const fn = obj.greet.bind(obj);
fn();  // "Hello Ali"

یا در پروژه‌های مدرن، استفاده از arrow function که this را از اسکوپ بیرونی به ارث می‌برد:

const obj = {
  name: "Ali",
  greet: () => "Hello " + obj.name
};

در بافت رخداد (event handling)، این الگو بسیار شایع است. وقتی یک متد را به‌عنوان callback به یک رخداد می‌دهید، در لحظه اجرا، this به عنصر رخداد اشاره می‌کند، نه به شیء اصلی. راه‌حل استاندارد، استفاده از bind یا arrow function است. برای درک دقیق‌تر این رفتار در بافت شیءگرایی، مطالعه «شی گرایی در جاوااسکریپت» توصیه می‌شود.

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

در JavaScript، متد بدون این، یک تابع معمولی است که هویت شیء خودش را از دست داده است.

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

فرض کنید همین امروز یک خطای is not a function در محیط تولید ظاهر شده و می‌خواهید ریشه‌اش را پیدا کنید. روشی که در این نوع پرونده‌ها به کار می‌گیرم، پنج گام دارد و هر گام، یک شرط را در ذهن من حذف می‌کند.

گام اول: نگاه دقیق به پیام و نام تابع

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

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

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

گام سوم: بررسی typeof مقدار

سومین کاری که می‌کنم، بررسی typeof مقداری است که خطا روی آن رخ داده. این گام به من می‌گوید که مقدار واقعی چه جنسی دارد. اگر typeof مقدار undefined باشد، مشکل از مقداردهی است. اگر typeof مقدار object باشد، مشکل از پراپرتی است که تابع نیست. اگر typeof مقدار string یا number باشد، مشکل از نوع داده است. این گام ساده، در تجربه من به‌شکل چشمگیری مسیر تشخیص را روشن می‌کند.

گام چهارم: بازتولید در کنسول

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

گام پنجم: بررسی نسخه و محیط

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

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

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

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

الگوی اول: بررسی typeof پیش از فراخوانی

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

if (typeof fn === "function") {
  fn();
}

این الگو در پروژه‌هایی که با callback کار می‌کنند، بسیار مفید است؛ چون به‌جای شکستن برنامه، یک مقدار پیش‌فرض یا پیام هشدار می‌دهد.

الگوی دوم: مقدار پیش‌فرض تابع خالی

در بافت پارامترهای اختیاری، می‌توانید مقدار پیش‌فرض یک تابع خالی بدهید:

function process(data, callback = () => {}) {
  callback(data);
}

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

الگوی سوم: استفاده از optional call

در ECMAScript 2020، عملگر ?. که در بافت فراخوانی به‌شکل fn?.() استفاده می‌شود، این امکان را می‌دهد که اگر مقدار undefined بود، فراخوانی انجام نشود:

fn?.();

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

الگوی چهارم: bind صریح در بافت رخداد

در بافت رخداد، استفاده از bind صریح، خطاهای ناشی از از دست رفتن this را حذف می‌کند:

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

یا در پروژه‌های مدرن، استفاده از arrow function که this را از بیرون به ارث می‌برد.

الگوی پنجم: تبدیل شبه‌آرایه به آرایه

در بافت شبه‌آرایه‌ها، تبدیل صریح به آرایه پیش از استفاده از متدهای آرایه‌ای، خطاها را حذف می‌کند:

Array.from(nodes).map(n => n.textContent);

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

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

const schema = z.object({ handler: z.function() });
const data = schema.parse(input);
data.handler();

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

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

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

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

  • استفاده از try/catch بدون درک ریشه. try/catch فقط جلوی سقوط را می‌گیرد؛ منبع خطا در جای دیگری ظاهر می‌شود و برنامه به‌شکل خاموش خراب می‌کند.
  • تبدیل undefined به تابع خالی بدون تفکیک. هر undefined به‌معنای «callback اختیاری» نیست. در بعضی موارد، undefined به‌معنای «خطای بالادست» است و تبدیل کورکورانه، اطلاعات را از بین می‌برد.
  • نادیده گرفتن نام‌گذاری‌ها. نام‌گذاری‌های نامناسب مانند fn، cb، x باعث می‌شود در زمان خطا نتوانید سریع ریشه را پیدا کنید.
  • مقداردهی دیرهنگام متدها. اگر متدی در زمان اجرا روی شیء اضافه می‌شود، همیشه مطمئن شوید که قبل از اولین فراخوانی، اضافه شده است.
  • مخلوط کردن مدل داده و رفتار. در طراحی مدل‌های شیء، همیشه بین داده و رفتار تفکیک صریح بگذارید. این تفکیک، جلوی بسیاری از خطاها را می‌گیرد.
  • نادیده گرفتن تفاوت نسخه‌ها. بعضی از این خطاها در نسخه‌های قدیمی‌تر کتابخانه‌ها ظاهر می‌شوند و در نسخه‌های جدیدتر رفع شده‌اند. همیشه نسخه‌ها را در نظر بگیرید.
  • نداشتن تست واحد برای مرزهای فراخوانی. اگر تست‌های شما فقط مسیرهای موفق را پوشش می‌دهند، خطاهای فراخوانی در تولید ظاهر می‌شوند.

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

ماتریس تست برای فراخوانی تابع

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

سناریوورودیخروجی مورد انتظار
فراخوانی تابع معتبرتابعاجرا بدون خطا
فراخوانی undefinedundefinedTypeError
فراخوانی nullnullTypeError
فراخوانی رشته"hello"TypeError
فراخوانی عدد42TypeError
فراخوانی آرایه[1,2,3]TypeError
فراخوانی شیء بدون call{}TypeError
فراخوانی متد جدا افتادهobj.methodبسته به content متد
فراخوانی پس از bindobj.method.bind(obj)اجرا بدون خطا
فراخوانی با optional callfn?.()اجرا یا نادیده‌گیری

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

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

پرسش‌های پرتکرار درباره is not a function

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

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

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

چرا فراخوانی متد جدا افتاده خطا می‌دهد ولی فراخوانی روی شیء خطا نمی‌دهد؟

چون مقدار this در زمان فراخوانی مشخص می‌شود. وقتی متد را از شیء جدا می‌کنید، this به شیء اصلی اشاره نمی‌کند و در نتیجه متد نمی‌تواند به پراپرتی‌های آن دسترسی داشته باشد. راه‌حل، استفاده از bind یا arrow function است.

آیا optional call در همه موتورها پشتیبانی می‌شود؟

در موتورهای مدرن، بله. در مرورگرهای قدیمی‌تر، معمولاً با transpiler و polyfill قابل استفاده است، ولی رفتار ممکن است در جزئیات متفاوت باشد. تست در محیط هدف ضروری است.

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

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

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

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

آیا هنگام import، این خطا در زمان بارگذاری داده می‌شود؟

در بعضی از باندلرها، خطا در زمان بارگذاری رخ می‌دهد و در بعضی دیگر، در زمان فراخوانی. تفاوت این دو، در نحوه bundling وابسته است. توصیه من، تست explicit import در ابتدای ماژول است تا خطا در زمان بارگذاری گرفته شود.

آیا تبدیل arguments به آرایه همیشه لازم است؟

اگر از function استفاده می‌کنید و می‌خواهید متدهای آرایه‌ای را روی arguments اعمال کنید، بله. راه‌حل مدرن‌تر، استفاده از rest parameters است که از ES6 اضافه شد و نیازی به تبدیل ندارد.

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

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

لایه اول: قرارداد صریح برای callbackها

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

لایه دوم: تفکیک صریح داده از رفتار

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

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

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

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

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

یک عادت کوچک، یک کلاس خطای قابل پیش‌بینی

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

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