یک بار، در پروژه‌ای که یک داشبورد تحلیلی می‌ساختم، همه‌چیز در محیط تست بی‌نقص کار می‌کرد ولی در production، بعد از لاگین کاربر، صفحه سفید می‌شد و در کنسول مرورگر یک پیام ساده دیده می‌شد: TypeError: Cannot read properties of null (reading "name"). تا آن روز، خطای null در JavaScript را به‌عنوان یک مشکل تایپی می‌شناختم که با بررسی مقدار حل می‌شود. آن روز فهمیدم که این خطا در باطن، پنجره‌ای است به سمت تفکر مرزی، مدیریت وضعیت برنامه، و انضباط در کار با DOM و داده‌های asynchronous.

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

JavaScript یک زبان با نوع‌شناسی پویا (dynamic typing) است که در آن متغیرها می‌توانند در زمان اجرا هر نوع مقداری داشته باشند. یکی از این مقادیر، null است که به‌عنوان «هیچ مقداری» در نظر گرفته می‌شود. وقتی کد شما سعی می‌کند به property یا متد یک null دسترسی بزند، JavaScript خطای زیر را مطرح می‌کند:

TypeError: Cannot read properties of null (reading "name")

TypeError: Cannot read property "name" of null

TypeError: null is not an object

شکل دقیق پیام خطا بستگی به موتور JavaScript دارد: V8 (Chrome/Node.js)، SpiderMonkey (Firefox)، و JavaScriptCore (Safari) پیام‌های متفاوتی تولید می‌کنند. ولی مفهوم در همه یکسان است: null یک شیء نیست و دسترسی به property آن ممکن نیست.

نکته‌ی مهم: این خطا از نوع TypeError است، نه ReferenceError. تفاوت این دو مهم است: ReferenceError وقتی رخ می‌دهد که متغیر وجود ندارد. TypeError وقتی رخ می‌دهد که متغیر وجود دارد ولی نوع آن با عملیات سازگار نیست. در مورد null، متغیر وجود دارد (مقدارش null است) ولی نمی‌توان روی آن عملیات property انجام داد.

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

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

تفاوت null و undefined

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

undefined: وقتی رخ می‌دهد که متغیری تعریف شده ولی مقدار نگرفته، یا به property‌ای دسترسی می‌زنید که وجود ندارد، یا یک تابع بدون return صریح، پایان می‌یابد. در واقع، undefined «نبودِ تعریف» را نشان می‌دهد.

null: یک مقدار صریح است که برنامه‌نویس برای نشان دادن «هیچ مقدار» استفاده می‌کند. مثلاً وقتی یک تابع، نتیجه‌ای برای برگرداندن ندارد، ممکن است null برگرداند.

تفاوت‌های کلیدی:

معیار null undefined
نوع object (نقص تاریخی) undefined
تخصیص صریح بله خیر
در JSON صریح ذخیره می‌شود حذف می‌شود
پیش‌فرض پارامتر خیر بله
در JSON.stringify null می‌ماند حذف می‌شود

نکته‌ی ظریف: در JavaScript، typeof null مقدار "object" برمی‌گرداند که یک نقص تاریخی از روزهای اولیه‌ی این زبان است. برای تشخیص دقیق، از value === null استفاده کنید.

در خطای null، JavaScript بین این دو تفاوت قائل می‌شود:

let a = null;
a.name;  // TypeError: Cannot read properties of null

let b;
b.name;  // TypeError: Cannot read properties of undefined

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

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

JavaScript به‌طور طراحی‌شده، دسترسی به property یک null را به‌عنوان خطا در نظر می‌گیرد. این تصمیم، از چند اصل بنیادین می‌آید:

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

دو: رفتار امن در برابر باگ. دسترسی به property یک null، نشانه‌ی یک فرض نادرست در کد است. JavaScript این فرض را در همان نقطه افشا می‌کند تا دیباگ ساده‌تر شود.

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

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

نُه علت رایج این خطا

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

  1. دسترسی به DOM قبل از آمادگی: کد در <head> اجرا می‌شود قبل از بارگذاری DOM.
  2. سلکتور اشتباه در querySelector: عنصر مورد نظر وجود ندارد.
  3. داده‌ی asynchronous که هنوز نرسیده: کد روی response خالی کار می‌کند.
  4. دسترسی به property یک شیء null: فرض شیء بودن چیزی که null است.
  5. null در آرایه و حلقه‌ها: null بودن عناصر آرایه.
  6. JSON.parse و پاسخ‌های خالی: پردازش JSON نامعتبر یا خالی.
  7. تابع بازگشت‌دهنده‌ی null: تابعی که در شرایط خاص null برمی‌گرداند.
  8. null در localStorage/sessionStorage: خواندن مقدار غیرموجود از storage.
  9. null در فریم‌ورک‌ها: React، Vue، Angular با lifecycle و state پیچیده.

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

دسترسی به DOM قبل از آمادگی

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

<!DOCTYPE html>
<html>
<head>
    <script>
        const button = document.querySelector("#myButton");
        button.addEventListener("click", handleClick);
        // TypeError: Cannot read properties of null
    </script>
</head>
<body>
    <button id="myButton">Click</button>
</body>
</html>

در این مثال، کد در <head> اجرا می‌شود، قبل از اینکه عنصر <button> در DOM ظاهر شود. بنابراین document.querySelector("#myButton") مقدار null برمی‌گرداند.

راه‌حل‌ها:

راه اول: قرار دادن اسکریپت در انتهای body

<body>
    <button id="myButton">Click</button>
    <script>
        const button = document.querySelector("#myButton");
        button.addEventListener("click", handleClick);
    </script>
</body>

راه دوم: استفاده از DOMContentLoaded

document.addEventListener("DOMContentLoaded", () => {
    const button = document.querySelector("#myButton");
    button.addEventListener("click", handleClick);
});

راه سوم: استفاده از defer در تگ script

<script src="app.js" defer></script>

ویژگی defer باعث می‌شود اسکریپت بعد از parse کامل DOM اجرا شود.

نکته‌ی ظریف: استفاده از defer رویکرد مدرن و ترجیحی است، چون این ویژگی در <head> هم کار می‌کند و از مسدود کردن parse HTML جلوگیری می‌کند. مبانی DOM در کار با DOM در جاوااسکریپت آمده است.

سلکتور اشتباه در querySelector

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

const button = document.querySelector("#my-button");
button.addEventListener("click", handleClick);
// TypeError اگر عنصر با id "my-button" وجود نداشته باشد

دلایل رایج:

  • اشتباه تایپی در id: myButton به‌جای my-button.
  • تغییر id در HTML: id در HTML تغییر کرده ولی در JavaScript نه.
  • سلکتور نادرست: استفاده از . برای id یا # برای class.
  • عنصر در iframe یا Shadow DOM: عنصر در محدوده‌ی document ریشه نیست.

راه‌حل: همیشه قبل از دسترسی، وجود عنصر را بررسی کنید:

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

نکته‌ی ظریف: در محیط development، خطای null به‌سرعت کشف می‌شود. در production، ممکن است در شرایط خاص ظاهر شود (مثلاً اگر CMS عنصر را در شرایط خاص حذف کرده باشد). همیشه بررسی صریح داشته باشید.

داده‌ی asynchronous که هنوز نرسیده

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

let userData = null;

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

// در جای دیگری از کد
console.log(userData.name);
// TypeError: Cannot read properties of null

در این مثال، کد به‌طور همزمان با fetch اجرا می‌شود، در حالی که داده‌ی user هنوز بارگذاری نشده است. بنابراین userData هنوز null است.

راه‌حل‌ها:

راه اول: مدیریت در then

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

راه دوم: استفاده از async/await

async function loadUser() {
    const response = await fetch("/api/user");
    const data = await response.json();
    console.log(data.name);
}

loadUser();

راه سوم: حالت loading صریح

let userData = null;
let isLoading = true;

async function loadUser() {
    try {
        const response = await fetch("/api/user");
        userData = await response.json();
    } catch (error) {
        console.error(error);
    } finally {
        isLoading = false;
    }
}

function renderUser() {
    if (isLoading) {
        return "Loading...";
    }
    if (!userData) {
        return "User not found";
    }
    return userData.name;
}

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

دسترسی به property یک شیء null

چهارمین منبع، دسترسی به property یک شیء null است. الگوی کلاسیک:

function getUsername(response) {
    return response.data.user.name;
}

getUsername(null);
// TypeError: Cannot read properties of null (reading "data")

در این مثال، تابع انتظار یک شیء response دارد ولی null دریافت کرده است.

راه‌حل: بررسی صریح یا استفاده از Optional Chaining:

function getUsername(response) {
    return response?.data?.user?.name ?? "Guest";
}

این رویکرد، در کدی که با داده‌ی خارجی کار می‌کند، ضروری است. جزئیات کامل Optional Chaining در بخش‌های بعدی این مقاله آمده است.

null در آرایه و حلقه‌ها

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

const users = [user1, null, user2, null];

users.forEach(user => {
    console.log(user.name);
    // TypeError در iteration‌های null
});

راه‌حل: فیلتر کردن مقادیر null یا بررسی در حلقه:

users
    .filter(user => user !== null)
    .forEach(user => {
        console.log(user.name);
    });

یا با Optional Chaining:

users.forEach(user => {
    console.log(user?.name);
});

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

JSON.parse و پاسخ‌های خالی

ششمین منبع، JSON.parse و پاسخ‌های خالی است. الگوی کلاسیک:

const stored = localStorage.getItem("user");
const user = JSON.parse(stored);
console.log(user.name);
// TypeError اگر stored null یا نامعتبر باشد

در این مثال، اگر localStorage.getItem مقدار null برگرداند، JSON.parse(null) مقدار null برمی‌گرداند و دسترسی به name خطا می‌دهد.

راه‌حل: بررسی مقدار قبل از parse:

const stored = localStorage.getItem("user");
let user = null;

if (stored) {
    try {
        user = JSON.parse(stored);
    } catch (error) {
        console.error("Invalid JSON:", error);
    }
}

if (user) {
    console.log(user.name);
}

این رویکرد، در پردازش داده‌ی ذخیره‌شده در storage یا پاسخ‌های API، ضروری است. مبانی JSON در JSON چیست و چگونه داده‌ها را ساختاردهی می‌کند آمده است.

تابع بازگشت‌دهنده‌ی null

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

function findUser(id) {
    return users.find(user => user.id === id) || null;
}

const user = findUser(999);
console.log(user.name);
// TypeError: Cannot read properties of null

در این مثال، findUser در صورت نبود کاربر، null برمی‌گرداند و کد فراخوان، این null را بررسی نمی‌کند.

راه‌حل: بررسی مقدار بازگشتی یا استفاده از Optional Chaining:

const user = findUser(999);
console.log(user?.name ?? "User not found");

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

null در React، Vue و Angular

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

در React

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

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

در React، از useRef برای دسترسی به DOM استفاده می‌کنید. اگر ref به عنصر متصل نشده باشد، ref.current مقدار null است:

function MyComponent() {
    const inputRef = useRef(null);
    
    useEffect(() => {
        if (inputRef.current) {
            inputRef.current.focus();
        }
    }, []);
    
    return <input ref={inputRef} />;
}

در Vue

<template>
    <div v-if="user">{{ user.name }}</div>
</template>

<script>
export default {
    props: ["user"],
};
</script>

در Angular

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

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

روش تشخیص اصولی در چهار گام

در تجربه‌ی من، تشخیص خطای null در چند ثانیه انجام می‌شود، اگر روش سیستماتیک داشته باشید:

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

گام دوم: بررسی مقدار شیء. در نقطه‌ی خطا، مقدار شیء را لاگ کنید:

console.log("Response:", response);
console.log("Response type:", typeof response);
console.log("Response is null:", response === null);
return response.data.user.name;

این رویکرد، فوراً نشان می‌دهد که چرا خطا رخ داده است. در بیشتر موارد، شیء null است یا هنوز بارگذاری نشده.

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

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

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

  • console.log و console.trace: چاپ اطلاعات.
  • Chrome DevTools: با Pause on exceptions.
  • Firefox Developer Tools: با Pause on exceptions.
  • VS Code Debugger: با breakpoint و inspection.
  • ESLint: با قواعد no-undef و no-unused-vars.

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

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

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

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

ساده‌ترین و موثرترین راه‌حل:

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

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

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

try {
    console.log(user.name);
} catch (error) {
    console.error("Access failed:", error);
}

این رویکرد، در کدهای پیچیده‌تر مناسب است. ولی در JavaScript، try/catch هزینه‌ی performance دارد، بنابراین توصیه می‌شود در مواردی که احتمال خطا بالاست استفاده شود.

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

const user = response?.data?.user ?? { name: "Guest" };
console.log(user.name);

این رویکرد، در کدی که با داده‌ی اختیاری کار می‌کند، عالی است.

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

در بعضی موارد، خطای null نشانه‌ی مشکل منطقی است. مثلاً فرض وجود داده‌ی async قبل از بارگذاری. بازنویسی منطق با state مدیریت‌شده، بهترین راه‌حل است.

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

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

Optional Chaining و Nullish Coalescing

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

Optional Chaining (?.)

اجازه می‌دهد به property یک شیء دسترسی بزنید بدون خطا در صورت null یا undefined:

const name = user?.name;
// اگر user null باشد، name undefined است، نه خطا

const city = user?.address?.city;
// زنجیره‌ای، در هر سطح

const first = arr?.[0];
// روی آرایه

const result = func?.();
// روی تابع

Nullish Coalescing (??)

مقدار پیش‌فرض تعیین می‌کند فقط در صورت null یا undefined:

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

// تفاوت با ||
const value1 = 0 || "default";  // "default" (اشتباه)
const value2 = 0 ?? "default";  // 0 (درست)

نکته‌ی ظریف: اپراتور || مقادیر falsy مثل 0، ""، false را هم جایگزین می‌کند. اپراتور ?? فقط null و undefined را. این تفاوت در محاسبات حساس مهم است.

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

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

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

Type Guards و بررسی صریح

Type Guards در TypeScript و JavaScript، توابعی هستند که نوع یک متغیر را بررسی می‌کنند. در JavaScript خالص، این رویکرد با توابع بررسی صریح انجام می‌شود:

function isNotNull(value) {
    return value !== null && value !== undefined;
}

function isUserObject(value) {
    return isNotNull(value) && typeof value === "object" && "name" in value;
}

if (isUserObject(user)) {
    console.log(user.name);
}

در TypeScript، Type Guards صریح‌تر هستند:

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

if (isUser(data)) {
    console.log(data.name);  // TypeScript می‌داند data از نوع User است
}

این رویکرد، در پردازش داده‌ی خارجی (API، localStorage، URL params) ضروری است. مبانی کار با داده در کار با JSON در پروژه‌های واقعی آمده است.

TypeScript و پیشگیری در زمان compile

TypeScript با type system قدرتمند، بخش بزرگی از خطاهای null را قبل از اجرا تشخیص می‌دهد. مهم‌ترین ویژگی، strictNullChecks است که در tsconfig.json فعال می‌شود:

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

با فعال بودن این گزینه، TypeScript بین null، undefined و مقادیر معتبر تفاوت قائل می‌شود:

interface User {
    name: string;
    email: string;
}

function getUsername(user: User | null): string {
    return user.name;
    // Error: Object is possibly null
}

// راه‌حل
function getUsername(user: User | null): string {
    if (!user) return "Guest";
    return user.name;
}

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

این خطا در محیط production

در محیط production، خطای null ابعاد جدی‌تری دارد:

قطع سرویس

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

نشت اطلاعات

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

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

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

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

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

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

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

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

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

اشتباه اول: try/catch به‌عنوان راه‌حل عمومی

استفاده‌ی بی‌محابای try/catch برای هر دسترسی، کد را غیرخوانا می‌کند و performance را کاهش می‌دهد. راه‌حل: در موارد عادی از بررسی صریح استفاده کنید.

اشتباه دوم: نادیده گرفتن === null

استفاده از if (!value) به‌جای if (value === null) می‌تواند مقادیر falsy مثل 0 و "" را هم پوشش دهد. راه‌حل: بررسی دقیق با ===:

if (value === null || value === undefined) {
    // فقط null و undefined
}

// یا با اپراتور مدرن
if (value == null) {
    // هم null و هم undefined (بدون type checking)
}

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

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

اشتباه چهارم: نبود تست برای null

اگر تست‌ها فقط برای داده‌ی معتبر نوشته شوند، خطاهای null در production کشف می‌شوند. راه‌حل: برای هر تابع، تست‌های edge case بنویسید: null، undefined، شیء خالی، و مقادیر مرزی.

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

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

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

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

{
    "rules": {
        "no-undef": "error",
        "no-unused-vars": "error",
        "no-implicit-globals": "error",
        "no-return-assign": "error"
    }
}

اشتباه هفتم: نبود error boundary در فریم‌ورک‌ها

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

class ErrorBoundary extends React.Component {
    state = { hasError: false };
    
    static getDerivedStateFromError(error) {
        return { hasError: true };
    }
    
    componentDidCatch(error, info) {
        console.error(error, info);
    }
    
    render() {
        if (this.state.hasError) {
            return <h1>Something went wrong.</h1>;
        }
        return this.props.children;
    }
}
خطای null یک پیام از فرض است، نه از داده. JavaScript می‌گوید این شیء null است ولی کد شما فرض کرده معتبر است. راه‌حل، بررسی صریح است نه سرکوب خطا.

پرسش‌های پرتکرار درباره خطای null در JavaScript

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

تفاوت null و undefined چیست؟

null یک مقدار صریح است که برنامه‌نویس برای «هیچ مقدار» استفاده می‌کند. undefined نشان می‌دهد متغیری تعریف شده ولی مقدار نگرفته. تفاوت‌ها: typeof null مقدار "object" برمی‌گرداند (نقص تاریخی)، در JSON null ذخیره می‌شود ولی undefined حذف می‌شود، و پیش‌فرض پارامترهای تابع undefined است نه null.

آیا == و === در بررسی null تفاوت دارند؟

بله. value == null هم null و هم undefined را می‌گیرد (چون == تبدیل نوع انجام می‌دهد). value === null فقط null را می‌گیرد. برای بررسی دقیق از === استفاده کنید.

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

چون در محیط development، داده‌های تست معمولاً معتبر هستند. در production، داده‌ی واقعی می‌تواند null باشد (مثلاً کاربر بدون پروفایل، یا API بدون پاسخ). راه‌حل: تست با داده‌های مرزی و استفاده از TypeScript.

آیا Optional Chaining در همه‌ی مرورگرها کار می‌کند؟

بله، در مرورگرهای مدرن (Chrome 80+، Firefox 74+، Safari 13.1+، Edge 80+). برای مرورگرهای قدیمی، نیاز به transpile با Babel یا استفاده از جایگزین مثل lodash.get دارید.

تفاوت ?. و ?? چیست؟

?. (Optional Chaining) برای دسترسی ایمن به property استفاده می‌شود. ?? (Nullish Coalescing) برای تعیین مقدار پیش‌فرض در صورت null یا undefined استفاده می‌شود. می‌توانید ترکیبشان کنید:

const name = user?.profile?.name ?? "Guest";

چرا در React، خطای null شایع است؟

چون در React، state و props می‌توانند در لحظه‌ی اول null باشند (مثلاً قبل از دریافت پاسخ API). راه‌حل: بررسی صریح در component یا استفاده از conditional rendering.

آیا می‌توانم از try/catch برای خطای null استفاده کنم؟

بله، ولی توصیه می‌شود. در JavaScript، try/catch هزینه‌ی performance دارد. در موارد عادی، از بررسی صریح استفاده کنید:

if (user !== null) {
    // استفاده از user
}

چگونه از این خطا در کد async پیشگیری کنم؟

با مدیریت صریح خطا در async/await:

async function loadUser() {
    try {
        const response = await fetch("/api/user");
        if (!response.ok) throw new Error("Failed");
        const data = await response.json();
        return data ?? null;
    } catch (error) {
        console.error(error);
        return null;
    }
}

آیا TypeScript این خطا را حل می‌کند؟

TypeScript با strictNullChecks بخش بزرگی از این خطاها را در زمان compile تشخیص می‌دهد. ولی TypeScript نمی‌تواند همه‌ی خطاها را کشف کند (مخصوصاً در مرزهای runtime). ترکیب TypeScript با تست و monitoring بهترین رویکرد است.

تفاوت خطای null و خطای undefined چیست؟

خطای null وقتی رخ می‌دهد که دسترسی به property یک null باشد. خطای undefined وقتی رخ می‌دهد که دسترسی به property یک undefined باشد. الگوی حل مشابه است ولی پیام‌های خطا متفاوت هستند. جزئیات کامل در خطای undefined در جاوااسکریپت آمده است.

چرا typeof null === "object" است؟

این یک نقص تاریخی در JavaScript است. در نسخه‌های اولیه، مقادیر در یک word 32-bit ذخیره می‌شدند که 3 بیت اول آن نوع را نشان می‌داد. null با تمام بیت‌های صفر نمایش داده می‌شد که با نوع object یکسان بود. این نقص تا امروز حفظ شده برای سازگاری با کدهای قدیمی.

آیا می‌توانم از Array.prototype.filter(Boolean) برای حذف null استفاده کنم؟

بله، این رویکرد در فیلتر کردن مقادیر falsy مفید است:

const users = [user1, null, user2, undefined];
const validUsers = users.filter(Boolean);
// [user1, user2]

ولی توجه کنید که این رویکرد همه‌ی مقادیر falsy (0، ""، false) را هم حذف می‌کند. اگر فقط null و undefined می‌خواهید حذف کنید:

const validUsers = users.filter(user => user != null);

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

با conditional rendering و بررسی صریح:

<template>
    <div v-if="user">{{ user.name }}</div>
    <div v-else>Loading...</div>
</template>

مبانی Vue در Vue.js برای مبتدیان آمده است.

آیا document.querySelector همیشه null را برمی‌گرداند اگر عنصر وجود نداشته باشد؟

بله. querySelector و getElementById در صورت نبود عنصر، null برمی‌گردانند. همیشه بررسی کنید:

const element = document.querySelector("#myElement");
if (element) {
    // استفاده از element
}

چگونه از این خطا در SSR پیشگیری کنم؟

در Server-Side Rendering (Next.js، Nuxt.js)، کدهایی که به window، document، یا localStorage دسترسی دارند، در سرور null هستند. راه‌حل: بررسی typeof window !== "undefined":

if (typeof window !== "undefined") {
    const user = localStorage.getItem("user");
}

آیا null روی آرایه هم مشکل است؟

null روی آرایه، به‌طور خالص مشکل نیست. [1, null, 2] یک آرایه‌ی معتبر است. مشکل وقتی رخ می‌دهد که روی عنصر null عملیات property انجام دهید. راه‌حل: فیلتر کردن یا بررسی عناصر در حلقه.

چگونه در Jest، خطای null را تست کنم؟

با toThrow:

test("throws on null access", () => {
    const user = null;
    expect(() => user.name).toThrow(TypeError);
});

آیا Object.is(null, null) مقدار درست برمی‌گرداند؟

بله، Object.is(null, null) مقدار true برمی‌گرداند. Object.is یک روش دقیق‌تر از === برای مقایسه است، ولی در مورد null، تفاوت ندارند.

آیا می‌توانم از void 0 به‌جای undefined استفاده کنم؟

void 0 همیشه undefined برمی‌گرداند. این رویکرد در کدهای قدیمی برای سازگاری با مرورگرهایی که undefined را بازنویسی می‌کردند، استفاده می‌شد. در مرورگرهای مدرن، نیازی به آن نیست.

چگونه در Angular، از این خطا پیشگیری کنم؟

Angular با strict mode و strictNullChecks، بخش بزرگی از خطاهای null را در زمان compile تشخیص می‌دهد. همچنین، از async pipe و *ngIf برای conditional rendering استفاده کنید. مبانی Angular در مباحث JavaScript آمده است.

آیا null در JSON.stringify تفاوت دارد؟

JSON.stringify({ a: null, b: undefined }) مقدار {"a":null} برمی‌گرداند. یعنی null حفظ می‌شود ولی undefined حذف می‌شود. این تفاوت در API‌ها و ذخیره‌سازی مهم است.

آیا خطای null روی performance تأثیر دارد؟

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

آنچه از سال‌ها کار با خطای null در JavaScript آموختم

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

یک: بررسی صریح همیشه بهتر از try/catch است. در 90 درصد موارد، بررسی if (value === null) یا if (value != null) کافی است و هم خواناتر است هم سریع‌تر. try/catch را برای موارد پیچیده نگه دارید.

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

سه: Optional Chaining و Nullish Coalescing، ابزارهای مدرن هستند. این دو اپراتور، کد را خواناتر و مقاوم‌تر می‌کنند. برای مرورگرهای قدیمی، از transpile استفاده کنید.

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

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

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