بار اول که این خطا را در یک پروژه جدی دیدم، در یک درخواست AJAX بود که پاسخ سرور، به‌جای JSON، یک صفحه HTML خطای ۵۰۰ برگردانده بود. کد سمت کلاینت با اطمینان کامل، پاسخ را به JSON.parse می‌داد و موتور جاوااسکریپت در همان حرف اول HTML، یعنی <، متوقف می‌شد. از آن روز، هر بار که این خطا را در کنسول می‌بینم، پیش از هر چیز سراغ خودِ داده می‌روم، نه سراغ کد.

خطای Unexpected token in JSON دقیقاً چیست؟

خطای Unexpected token in JSON یکی از پیام‌های استاندارد موتور جاوااسکریپت است که زمانی ظاهر می‌شود که کد شما می‌خواهد رشته‌ای را با JSON.parse به شیء تبدیل کند، ولی آن رشته، یک JSON معتبر نیست. جنس این خطا از خانواده SyntaxError است؛ یعنی نه خطای نوع، نه خطای محدوده، بلکه خطای نگارش داده. موتور در این لحظه به شما می‌گوید: «این داده، از قواعد نگارش JSON تبعیت نمی‌کند و من نمی‌توانم آن را به شیء تبدیل کنم.»

نکته‌ای که در نگاه اول پنهان می‌ماند این است که پیام دقیق این خطا، بسته به موتور، شکل‌های متفاوتی دارد. در موتور V8، شکل رایج Unexpected token X in JSON at position N است؛ در موتور SpiderMonkey، شکل JSON.parse: unexpected character at line X column Y؛ و در موتور JavaScriptCore سافاری، شکل JSON Parse error: Unexpected identifier. اگر با JSON و قواعد آن آشنایی کامل ندارید، پیشنهاد می‌کنم ابتدا مرور جامعی روی ساختار آن داشته باشید؛ مقاله «JSON چیست و چطور داده‌ها را در وب ساختاردهی می‌کند» نقطه شروع مناسبی است.

این خطا در ECMAScript به‌عنوان بخشی از پیاده‌سازی استاندارد JSON.parse تعریف شده است. طبق RFC 8259، ساختار JSON یک گرامر سخت‌گیرانه دارد که در آن، هیچ انعطافی برای تخطی از قواعد وجود ندارد. این سخت‌گیری، برخلاف JavaScript که در بسیاری از بافت‌ها بخشنده است، انتخاب عامدانه طراحان JSON بود تا داده‌ها بین زبان‌ها و پلتفرم‌ها به‌شکل قابل پیش‌بینی مبادله شوند.

JSON یک زبان مبادله داده است، نه یک زبان برنامه‌نویسی؛ و همین است که هیچ‌گونه انعطاف در نگارش آن وجود ندارد.

در تجربه من، این خطا در سه بافت اصلی ظاهر می‌شود: در بافت درخواست‌های شبکه که پاسخ سرور قابل اعتماد نیست؛ در بافت ذخیره‌سازی محلی مثل localStorage که داده قبلی ممکن است معتبر نباشد؛ و در بافت پیکربندی پروژه که فایل JSON ممکن است با ویرایش دستی خراب شده باشد. در هر سه بافت، مسئله مشترک است: داده‌ای که انتظار داریم معتبر باشد، معتبر نیست.

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

چرا JSON.parse این‌قدر سخت‌گیر است؟

یکی از پرتکرارترین سؤالاتی که در جلسات فنی مطرح می‌شود این است: چرا JSON.parse این‌قدر سخت‌گیر است؟ چرا همانند موتور جاوااسکریپت، بخشنده نیست؟ پاسخ در فلسفه طراحی JSON است. زمانی که Douglas Crockford این قالب را طراحی کرد، هدف این بود که داده‌ای سبک، قابل خواندن برای انسان و قابل پارس برای ماشین فراهم شود. اما آن‌چه بیشتر از خود قالب اهمیت داشت، پیش‌بینی‌پذیری بود.

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

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

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

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

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

خطای Unexpected token in JSON عضوی از خانواده بزرگ SyntaxError است. برای اینکه در پروژه‌های چندخطایی بتوانید سریع تشخیص دهید کدام SyntaxError را پیش رو دارید، بد نیست اعضای پرتکرار این خانواده را کنار هم ببینید:

پیاممعناسطح خطا
Unexpected token in JSONداده، JSON معتبر نیستسطح داده
Unexpected end of JSON inputداده ناقص استسطح داده
Unexpected identifierداده با ساختار اشتباه شروع شدهسطح داده
Invalid or unexpected tokenکاراکتر نامعتبر در کد یا دادهسطح کد یا داده
Unterminated string literalنقل‌قول بسته نشدهسطح کد یا داده

تفاوت کلیدی بین خطای JSON و سایر SyntaxErrorها این است که خطاهای JSON همیشه از داده می‌آیند، نه از کد. یعنی کد شما ممکن است کاملاً بی‌عیب باشد، ولی داده‌ای که به آن می‌رسد، معتبر نباشد. همین تفاوت، مسیر دیباگ را کاملاً تغییر می‌دهد: در خطاهای نگارشی کد، شما به کد نگاه می‌کنید؛ در خطاهای JSON، به داده نگاه می‌کنید.

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

قواعدی که بیشتر از همه نقض می‌شوند

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

تخلف اول: استفاده از نقل‌قول تکی

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

JSON.parse(`{ 'name': 'Ali' }`);
// SyntaxError: Unexpected token ' in JSON at position 2

این تخلف در داده‌هایی که از JavaScript به‌اشتباه به JSON تبدیل شده‌اند، بسیار شایع است. ابزارهایی مثل JSON.stringify همیشه نقل‌قول دوتایی تولید می‌کنند، ولی اگر داده به‌دست نوشته شده یا از منبعی غیراستاندارد آمده باشد، این تخلف رخ می‌دهد.

تخلف دوم: کاما انتهایی

در JSON، کاما انتهایی مجاز نیست. این تفاوت با JavaScript، یکی از پرتکرارترین منابع خطا است:

JSON.parse(`{ "a": 1, "b": 2, }`);
// SyntaxError: Unexpected token } in JSON at position 20

راه‌حل، حذف کاما انتهایی است یا استفاده از ابزارهای فرمت‌دهنده که این کاما را حذف می‌کنند. ابزارهایی مثل Prettier با تنظیمات مناسب، این نوع تخلف را در فایل‌های JSON جلوگیری می‌کنند.

تخلف سوم: کامنت‌گذاری

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

JSON.parse(`{
  // این کامنت نامعتبر است
  "name": "Ali"
}`);
// SyntaxError: Unexpected token / in JSON at position 6

راه‌حل استاندارد، استفاده از قالب‌هایی مثل JSON5 یا YAML است که کامنت را پشتیبانی می‌کنند. برای درک دقیق‌تر مزایا و کاربردهای YAML در بافت مدرن، مرور «کار با JSON در پروژه‌های واقعی» می‌تواند مفید باشد.

تخلف چهارم: مقادیر نامعتبر JavaScript

مقادیری مثل undefined، NaN، Infinity و تابع، در JSON وجود ندارند. اگر داده‌ای شامل این مقادیر باشد، خطا رخ می‌دهد:

JSON.parse(`{ "value": undefined }`);
// SyntaxError: Unexpected token u in JSON at position 12

نکته ظریف اینجاست که JSON.stringify خودش این مقادیر را حذف می‌کند؛ ولی اگر داده از منبع دیگری آمده باشد، این مقادیر می‌توانند در JSON ظاهر شوند. راه‌حل، تبدیل این مقادیر به null پیش از تولید JSON است.

تخلف پنجم: کلید بدون نقل‌قول

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

JSON.parse(`{ name: "Ali" }`);
// SyntaxError: Unexpected token n in JSON at position 2

این تخلف در داده‌هایی که از JavaScript به JSON تبدیل نشده‌اند، بسیار شایع است. راه‌حل، استفاده از JSON.stringify برای تولید JSON است.

تخلف ششم: کاراکترهای خاص در رشته

در JSON، کاراکترهای خاص مثل \n و \t باید با backslash فرار داده شوند. اگر داده شامل کاراکترهای خاص بدون escape باشد، خطا رخ می‌دهد:

JSON.parse(`{ "text": "خط اول
خط دوم" }`);
// در بعضی بافت‌ها خطا می‌دهد

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

تخلف هفتم: کاراکتر BOM در ابتدای داده

این تخلف، پنهان‌ترین و آزاردهنده‌ترین است. اگر داده‌ای که از یک فایل یا پاسخ سرور می‌آید، با BOM شروع شود، JSON.parse در همان بایت اول متوقف می‌شود:

const text = "\uFEFF{ \"a\": 1 }";
JSON.parse(text);
// SyntaxError: Unexpected token \uFEFF in JSON at position 0

این تخلف، در فایل‌های JSON که توسط ویرایشگرهای ویندوزی نوشته شده‌اند، بسیار شایع است. راه‌حل، حذف کاراکتر BOM در ابتدای رشته پیش از پارس است:

function stripBOM(text) {
  return text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
}
JSON.parse(stripBOM(text));

در تجربه من، این تخلف بیشتر از آن‌چه تصور می‌شود رخ می‌دهد، مخصوصاً در پروژه‌هایی که فایل‌های پیکربندی توسط اعضای مختلف تیم نوشته می‌شوند. اگر ابزارهایی مثل ESLint یا Prettier در پروژه شما فعال است، می‌توانید قاعده no-irregular-whitespace را فعال کنید تا این نوع تخلف زودتر گرفته شود.

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

دام بزرگ: وقتی پاسخ سرور HTML است

یکی از پرتکرارترین پرونده‌هایی که در پروژه‌های واقعی دیده‌ام، این است که پاسخ سرور به‌جای JSON، یک صفحه HTML است. این وضعیت در چند سناریو رخ می‌دهد:

  • سرور در پاسخ به درخواست API، یک صفحه خطای HTML برمی‌گرداند (مثلاً خطای ۵۰۰ یا ۵۰۲).
  • لایه‌ای از پروکسی یا WAF (Web Application Firewall) درخواست را مسدود می‌کند و صفحه HTML برمی‌گرداند.
  • آدرس API اشتباه است و درخواست به یک صفحه معمولی سایت می‌رسد.
  • سایت شما بدون احراز هویت، درخواست را به صفحه ورود هدایت می‌کند.

در همه این سناریوها، کد سمت کلاینت، پاسخ را بدون بررسی به JSON.parse می‌دهد و چون کاراکتر اول HTML یعنی < در JSON معتبر نیست، خطا رخ می‌دهد:

fetch("/api/data")
  .then(res => res.json())
  .then(data => console.log(data));
// SyntaxError: Unexpected token < in JSON at position 0

راه‌حل استاندارد، بررسی Content-Type پاسخ پیش از پارس است:

fetch("/api/data")
  .then(res => {
    const contentType = res.headers.get("content-type") || "";
    if (!contentType.includes("application/json")) {
      return res.text().then(text => {
        throw new Error(`Unexpected content type: ${contentType} | body: ${text.slice(0, 100)}`);
      });
    }
    return res.json();
  })
  .then(data => console.log(data));

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

یک نکته کاربردی دیگر: در بعضی مرورگرها، پیام این نوع خطا با دقت بیشتری نمایش داده می‌شود و به شما می‌گوید کاراکتر نامعتبر در موقعیت ۰ قرار دارد. اگر موقعیت ۰ و کاراکتر < بود، تقریباً همیشه این دام رخ داده است.

در بافت پاسخ‌های شبکه، همیشه قبل از پارس، نوع پاسخ را بررسی کنید؛ چون داده‌ای که به‌شکل HTML برمی‌گردد، ماهیت JSON ندارد.

دام‌های fetch و response.json()

متد response.json() در API مدرن، یک promise برمی‌گرداند که در صورت موفقیت، مقدار پارس‌شده را می‌دهد و در صورت خطا، یک SyntaxError برمی‌گرداند. این متد، سه دام مهم دارد که در پروژه‌ها زیاد دیده‌ام.

دام اول: پارس دوباره پاسخ

در پروژه‌های مبتنی بر framework، گاهی یک لایه از کد پاسخ را پارس می‌کند و لایه دیگری هم سعی می‌کند پارس کند. در این حالت، چون response.json() فقط یک بار قابل فراخوانی است، بار دوم خطا می‌دهد:

const res = await fetch("/api/data");
const data1 = await res.json();  // موفق
const data2 = await res.json();  // خطا

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

const res = await fetch("/api/data");
const resClone = res.clone();
const data1 = await res.json();
const data2 = await resClone.json();

دام دوم: پاسخ خالی

اگر پاسخ سرور بدنه‌ای نداشته باشد، response.json() خطای Unexpected end of JSON input می‌دهد. این وضعیت در درخواست‌های موفق با کد ۲۰۴ رخ می‌دهد:

const res = await fetch("/api/delete/1", { method: "DELETE" });
const data = await res.json();
// SyntaxError: Unexpected end of JSON input

راه‌حل استاندارد، بررسی طول بدنه یا کد وضعیت است:

const text = await res.text();
const data = text ? JSON.parse(text) : null;

دام سوم: پارس در زمان اشتباه

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

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

BOM، فاصله پنهان و کاراکترهای نامرئی

یکی از پرتکرارترین پرونده‌هایی که در دیباگ با آن مواجه می‌شوم، حضور کاراکترهای نامرئی در داده است. این کاراکترها در نگاه اول در ویرایشگر دیده نمی‌شوند، ولی موتور جاوااسکریپت آن‌ها را می‌بیند و در پارس داده دچار مشکل می‌شود.

BOM (Byte Order Mark)

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

function parseJSONSafe(text) {
  if (typeof text !== "string") return text;
  const cleaned = text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
  return JSON.parse(cleaned);
}

کاراکترهای فاصله نامرئی

بعضی کاراکترهای فاصله در یونیکد، در نگاه اول شبیه فاصله معمولی هستند ولی در JSON معتبر نیستند. مثلاً کاراکتر \u00A0 (fاصله غیرقابل‌شکستن) در بعضی ویرایشگرها به‌عنوان فاصله معمولی نمایش داده می‌شود، ولی موتور JSON آن را به‌عنوان کاراکتر نامعتبر می‌شناسد. راه‌حل، نرمال‌سازی داده پیش از پارس است:

function normalizeWhitespace(text) {
  return text.replace(/\u00A0/g, " ").replace(/\u200B/g, "");
}

کاراکترهای کنترلی

کاراکترهای کنترلی مثل \u0000 تا \u001F در JSON معتبر نیستند، مگر این‌که escape شوند. این کاراکترها در داده‌های باینری یا داده‌های وارد‌شده از سیستم‌های قدیمی ظاهر می‌شوند. راه‌حل استاندارد، پاک‌سازی یا escape این کاراکترها است:

function sanitizeControlChars(text) {
  return text.replace(/[\u0000-\u001F]/g, ch =>
    "\\u" + ch.charCodeAt(0).toString(16).padStart(4, "0")
  );
}

یک تکنیک ساده و مؤثر برای تشخیص این نوع کاراکترها، استفاده از ابزارهای نمایش کاراکترهای نامرئی در ویرایشگر است. در اکثر ویرایشگرهای مدرن مثل VS Code، می‌توانید با فعال کردن گزینه Render Whitespace این کاراکترها را ببینید. اگر با ابزارهای اشکال‌زدایی مرورگر کار می‌کنید، می‌توانید کاراکترهای نامعتبر را با String.charCodeAt شناسایی کنید؛ فهرست کامل‌تر این ابزارها در «ابزارهای اشکال‌زدایی جاوااسکریپت» جمع شده است.

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

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

فرض کنید همین امروز یک خطای Unexpected token in JSON در محیط تولید ظاهر شده و می‌خواهید ریشه‌اش را پیدا کنید. روشی که در این نوع پرونده‌ها به کار می‌گیرم، پنج گام دارد و هر گام، یک شرط را در ذهن من حذف می‌کند.

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

اولین کاری که می‌کنم، موقعیت کاراکتر در پیام خطا را دقیق می‌خوانم. اگر موقعیت ۰ باشد، مشکل در اولین کاراکتر داده است؛ یعنی یا BOM، یا کاراکتر HTML مثل <، یا فاصله غیرمجاز. اگر موقعیت بزرگ‌تر باشد، باید آن کاراکتر را در رشته پیدا کنم. این گام ساده، در تجربه من نیمی از زمان دیباگ را کم می‌کند.

گام دوم: لاگ کردن داده خام

دومین کاری که می‌کنم، لاگ کردن داده خام پیش از پارس است. این کار، مهم‌ترین گام در دیباگ این خطا است؛ چون بدون دیدن داده خام، فقط می‌توانید حدس بزنید. توصیه من، لاگ کردن داده با طول مشخص و حداکثر ۲۰۰ کاراکتر اول است:

console.log("Raw data:", text.slice(0, 200), "Length:", text.length);

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

گام سوم: شناسایی کاراکتر مشکل‌دار

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

const position = 5;
const char = text.charCodeAt(position);
console.log("Char code:", char.toString(16));

این کار، مخصوصاً در موارد BOM و کاراکترهای نامرئی، بسیار به کارم آمده است. اگر کد یونیکد کاراکتر feff بود، BOM است؛ اگر 00a0 بود، فاصله غیرقابل‌شکستن؛ و اگر 200b بود، کاراکتر فاصله صفر.

گام چهارم: بررسی منبع داده

چهارمین کاری که می‌کنم، بررسی منبع داده است. سه منبع اصلی وجود دارد: پاسخ شبکه، ذخیره‌سازی محلی، و فایل پیکربندی. اگر داده از پاسخ شبکه می‌آید، باید Content-Type و کد وضعیت را بررسی کنم. اگر از localStorage است، باید ببینم چه زمانی نوشته شده است. اگر از فایل پیکربندی است، باید آن فایل را باز کنم و کاراکترهای نامعتبر را جستجو کنم.

گام پنجم: بازتولید در کنسول

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

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

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

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

الگوی اول: try/catch محافظ

ساده‌ترین و در بسیاری از پروژه‌ها کافی‌ترین راه‌حل، پوشاندن پارس در یک try/catch است که در صورت شکست، مقدار پیش‌فرض برمی‌گرداند:

function safeParse(text, fallback = null) {
  try {
    return JSON.parse(text);
  } catch (error) {
    if (error instanceof SyntaxError) {
      console.warn("JSON parse failed:", error.message);
      return fallback;
    }
    throw error;
  }
}

نکته ظریف این الگو، محدود ماندن catch به SyntaxError است. اگر catch را باز بگذارید، هر خطای دیگری را هم بی‌سروصدا بلعیده‌اید و این خودش تبدیل به یک باگ بزرگ‌تر می‌شود.

الگوی دوم: پاک‌سازی پیش از پارس

اگر داده از منابع بیرونی می‌آید، می‌توانید پیش از پارس، BOM و کاراکترهای نامعتبر را حذف کنید:

function cleanJSON(text) {
  return text
    .replace(/^\uFEFF/, "")
    .replace(/[\u200B-\u200D\uFEFF]/g, "")
    .replace(/\u00A0/g, " ");
}

JSON.parse(cleanJSON(raw));

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

الگوی سوم: بررسی Content-Type پیش از پارس

در بافت پاسخ‌های شبکه، همیشه Content-Type را بررسی کنید:

if (!contentType.includes("application/json")) {
  throw new Error(`Expected JSON, got ${contentType}`);
}
return JSON.parse(text);

این الگو، دام پاسخ HTML به‌جای JSON را از پایه حل می‌کند و در پروژه‌هایی که با APIهای متعدد سروکار دارند، بسیار مفید است.

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

در پروژه‌های بزرگ، استفاده از کتابخانه‌هایی مثل json-parse-safe یا secure-json-parse توصیه می‌شود؛ چون علاوه بر پارس امن، از آسیب‌پذیری‌های prototype pollution هم جلوگیری می‌کنند:

import { parse } from "secure-json-parse";
const data = parse(text, null, { protoAction: "remove" });

الگوی پنجم: اعتبارسنجی با Schema

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

const schema = z.object({ id: z.number(), name: z.string() });
const data = schema.parse(JSON.parse(text));

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

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

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

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

  • پارس پاسخ بدون بررسی Content-Type. شایع‌ترین اشتباه. اگر پاسخ سرور HTML باشد، پارس بدون بررسی، به خطا منتهی می‌شود.
  • استفاده از eval به‌جای JSON.parse. در بعضی پروژه‌های قدیمی، به‌جای JSON.parse از eval استفاده می‌شود. این کار نه‌فقط امنیت را به‌خطر می‌اندازد، بلکه پیام‌های خطای گیج‌کننده‌تری تولید می‌کند.
  • پارس دوباره response.json(). فراخوانی دوباره این متد روی همان پاسخ، به خطا منتهی می‌شود.
  • نادیده گرفتن BOM. فایل‌های JSON که با ویرایشگرهای ویندوزی نوشته می‌شوند، ممکن است BOM داشته باشند و این خطا را فعال کنند.
  • استفاده از داده بدون اعتبارسنجی. حتی اگر پارس موفق شود، داده ممکن است ساختار اشتباهی داشته باشد. همیشه پس از پارس، اعتبارسنجی schema انجام دهید.
  • بلعیدن خطا در catch. اگر catch شما خطا را بی‌سروصدا بلعیده باشد، در لایه‌های بعدی به‌شکل باگ‌های مبهم‌تر ظاهر می‌شود.
  • نادیده گرفتن تفاوت موتورها. پیام‌های خطا در موتورهای مختلف متفاوتند. در پروژه‌های چندمرورگری، بررسی در همه محیط‌ها ضروری است.

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

پیامدهای امنیتی پارس نکردن JSON به‌شکل درست

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

حمله prototype pollution

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

const forbidden = new Set(["__proto__", "constructor", "prototype"]);
function safeAssign(target, source) {
  for (const key in source) {
    if (forbidden.has(key)) continue;
    target[key] = source[key];
  }
  return target;
}

افشای اطلاعات از طریق پیام‌های خطا

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

try {
  return JSON.parse(text);
} catch (error) {
  throw new Error("Data format error");
}

حمله denial of service از طریق داده بزرگ

در بعضی سناریوها، پارس داده JSON بسیار بزرگ می‌تواند باعث مصرف بی‌رویه حافظه و CPU شود. اگر ورودی از منبع غیرقابل اعتماد می‌آید، باید محدودیت طول و عمق تعریف کنید:

function parseWithLimits(text, maxLength = 1_000_000) {
  if (text.length > maxLength) {
    throw new Error("Data too large");
  }
  return JSON.parse(text);
}

یک نکته کاربردی: هر بار که در جلسات فنی بحث روی نمایش پیام خطا به کاربر پیش می‌آید، یادآوری کنید که خطاهای JSON یکی از پرخطرترین موارد برای افشای جزئیات داخلی است. توجه به این نکته در بازبینی کد، از بسیاری از نشت‌های اطلاعاتی جلوگیری می‌کند. در طراحی APIهای مدرن، استانداردهای امنیتی و راهنمای ساخت REST API می‌تواند به شما کمک کند تا این لایه را به‌شکل درست پیاده کنید؛ مرور «آموزش rest api» در این زمینه توصیه می‌شود.

ماتریس تست برای JSON.parse

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

سناریوورودیخروجی مورد انتظار
JSON معتبر ساده'{"a": 1}'شیء معتبر
JSON معتبر تودرتو'{"a": {"b": [1,2]}}'شیء معتبر
نقل‌قول تکی"{'a': 1}"SyntaxError
کاما انتهایی'{"a": 1,}'SyntaxError
کامنت'{"a": 1 /* x */}'SyntaxError
undefined'{"a": undefined}'SyntaxError
کلید بدون نقل‌قول'{a: 1}'SyntaxError
BOM در ابتدا'\uFEFF{"a": 1}'SyntaxError
پاسخ HTML'<html>...'SyntaxError
رشته خالی''SyntaxError

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

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

پرسش‌های پرتکرار درباره Unexpected token in JSON

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

تفاوت این خطا با Unexpected end of JSON input چیست؟

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

چرا پیام خطا در مرورگرها متفاوت است؟

چون هر موتور جاوااسکریپت، پیاده‌سازی خودش را دارد. V8 در Chrome و Node.js، SpiderMonkey در Firefox و JavaScriptCore در Safari، هر کدام پیام‌های متفاوتی تولید می‌کنند. توصیه من این است که برای تشخیص، روی موقعیت کاراکتر تمرکز کنید، نه روی متن دقیق پیام.

آیا می‌توانم از JSON.parse در بافت async استفاده کنم؟

بله. JSON.parse همگام است و در بافت async مشکلی ندارد. اگر داده حجیم است و می‌خواهید آن را در بافت جداگانه پارس کنید، می‌توانید از JSON.parse در یک worker استفاده کنید. برای درک دقیق‌تر رفتار async و Promise، مرور «async و await در جاوااسکریپت» توصیه می‌شود.

چرا در بعضی پروژه‌ها از eval به‌جای JSON.parse استفاده می‌شود؟

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

چطور بفهمم داده از BOM شروع می‌شود؟

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

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

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

آیا می‌توانم خطا را در لایه‌های بالاتر مدیریت کنم؟

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

آیا در APIهای گراف‌کیو هم این خطا رخ می‌دهد؟

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

نگاه معمارانه: پارس داده به‌عنوان قرارداد

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

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

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

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

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

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

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

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

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

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

خطای Unexpected token in JSON در نگاه اول یک خطای کوچک به‌نظر می‌رسد، اما در عمل، آینه‌ای است که نشان می‌دهد لایه داده پروژه شما چقدر صریح و کنترل‌شده است. اگر این خطا در تولید ظاهر می‌شود، به احتمال زیاد جای دیگری از سیستم هم داده‌ای بدون اعتبارسنجی جریان دارد. به همین دلیل، توصیه عملی من سه چیز است: اول، همیشه پیش از پارس، داده خام را لاگ کنید تا در زمان خطا، منبع روشن باشد؛ دوم، در مرزهای سیستم، یک لایه اعتبارسنجی متمرکز بگذارید؛ سوم، در تست‌های خود ماتریس سناریوهای پارس را بگنجانید تا رفتار برنامه در برابر داده‌های نامعتبر، قابل پیش‌بینی بماند.

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