من أكثر أخطاء جافاسكريبت شيوعًا عند التعامل مع أي API:
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
أو بصيغته القديمة التي ما زالت تظهر في إصدارات قديمة من Chrome وNode:
SyntaxError: Unexpected token < in JSON at position 0
الاسم يوحي بأن مشكلتك في JSON، والحقيقة في أغلب الحالات: ما وصلك ليس JSON أصلًا، وأول حرف منه فضح ذلك.
اقرأ البصمة: الحرف يخبرك بالسبب
كل رسالة في هذا الجدول نسخناها من تشغيل فعلي في Node.js 22 وChromium:
| الرسالة | ما وصلك فعليًا | السبب الغالب |
|---|---|---|
Unexpected token '<', "<!DOCTYPE "... | صفحة HTML | خطأ 404 أو 500، صفحة تسجيل دخول، رابط خاطئ |
"undefined" is not valid JSON | undefined أو النص "undefined" | قيمة غير موجودة، غالبًا من localStorage |
"[object Object]" is not valid JSON | كائن جاهز لا نص | تحليل بيانات محلَّلة مسبقًا |
Unexpected token 'I', "Internal S"... | رسالة خطأ نصية | Internal Server Error بلا JSON |
Unexpected token 'P', "Proxy erro"... | رسالة proxy | خادم التطوير لم يصل للـ API |
Unexpected end of JSON input | نص فارغ | رد بلا جسم، مثل 204 No Content |
Expected property name or '}' in JSON at position 1 | JSON مكسور | مفاتيح بلا " أو اقتباسات مفردة ' |
Expected double-quoted property name ... position 8 | JSON مكسور | فاصلة زائدة قبل } |
Unexpected non-whitespace character after JSON | قيمتان متتاليتان | كائنان ملصوقان، أو نص زائد بعد الـ JSON |
وفي المتصفحات الأخرى ترى المعنى نفسه بصياغة أخرى: Firefox يكتب
JSON.parse: unexpected character at line 1 column 1 of the JSON data، وSafari
يكتب JSON Parse error: Unrecognized token '<'.
قاعدة الموضع: position 0 أو line 1 column 1 يعني أن النص كله ليس JSON
(صفحة، أو رسالة، أو فراغ). أما موضع متقدم فيعني JSON حقيقيًا مكسورًا في ذلك
المكان بالضبط.
الخطوة الذهبية: اطبع النص الخام
قبل أي إصلاح، بدّل التحليل بالطباعة:
// ما كنت تفعله
const data = await res.json();
// التشخيص: ماذا وصل فعلًا؟
const raw = await res.text();
console.log(res.status, res.headers.get("content-type"));
console.log(raw.slice(0, 200)); // أول 200 حرف تكشف كل شيء
سترى فورًا <!DOCTYPE html>... أو نصًا فارغًا أو JSON فيه كسر واضح. وتبويب
Network في أدوات المطوّر (F12) يعرض الرد نفسه بنقرة: الحالة، والـ
Content-Type، والجسم.
انتبه: الجسم يُقرأ مرة واحدة. إن استدعيت res.json() بعد res.text() فلن
ترى خطأ JSON أصلًا، بل خطأ مختلفًا. جرّبناه: Chromium يكتب
TypeError: Failed to execute 'json' on 'Response': body stream already read،
وNode.js يكتب TypeError: Body is unusable: Body has already been read. اقرأ
بـ text() ثم حلّل النص بنفسك بـ JSON.parse(raw).
الحلول حسب الحرف
1) '<': وصلتك صفحة HTML، فالمشكلة في الطلب لا في التحليل
الخادم أعاد صفحة خطأ أو صفحة تسجيل دخول. افحص الحالة قبل التحليل دائمًا:
const res = await fetch("/api/lessons");
if (!res.ok) {
// 404؟ 500؟ الرسالة الحقيقية هنا، فلا تتركها تنفجر كخطأ JSON مضلّل
throw new Error(`API error ${res.status}: ${(await res.text()).slice(0, 200)}`);
}
const data = await res.json();
ثم صحّح السبب:
- الرابط: خطأ إملائي، أو مسار نسبي (
api/lessonsبلا/في البداية) يُحل بالنسبة للصفحة الحالية، فيصبح/blog/api/lessonsفي صفحة فرعية ويعود 404. - في تطبيق React على خادم تطوير Vite: مسار غير موجود يعيد صفحة التطبيق
نفسها (
index.html) بالحالة 200. جرّبناfetch("/api/lessons")على Vite 8 بلا خادم API خلفه، فعاد200 text/html. لهذاres.okصحيح والرد HTML. افحصContent-Type: إن لم يكنapplication/jsonفأنت تطلب المسار الخطأ، أو نسيت إعدادserver.proxyالذي يوجّه/apiللخادم الخلفي. - المصادقة: جلسة منتهية تحوّلك لصفحة الدخول.
- الخادم: حالة 500 تعني عطلًا عنده: حل خطأ 500. وإن كان الطلب محجوبًا بين نطاقين فالسبب CORS.
2) "undefined": قيمة غير موجودة
localStorage.setItem("user", undefined); // يخزّن النص "undefined"
JSON.parse(localStorage.getItem("user")); // "undefined" is not valid JSON
localStorage يحفظ النصوص فقط، فيحوّل undefined إلى الكلمة "undefined".
احفظ دائمًا بـ JSON.stringify واقرأ بأمان:
function readJSON(key, fallback = null) {
const raw = localStorage.getItem(key); // null إن لم يوجد المفتاح
if (raw === null || raw === "undefined") return fallback;
try {
return JSON.parse(raw);
} catch {
return fallback; // بيانات قديمة تالفة
}
}
ومن المفيد أن تعرف أن JSON.parse(null) لا يفشل، بل يعيد null، لأن null
تتحوّل للنص "null" وهو JSON صالح. لذلك يظهر الخطأ مع undefined لا مع
null. (والفرق بين القيمتين بالتفصيل في
الفرق بين undefined وnull.)
3) "[object Object]": تحلّل ما هو محلَّل أصلًا
const res = await fetch("/api/user");
const user = await res.json(); // كائن جاهز
JSON.parse(user); // "[object Object]" is not valid JSON
res.json() وaxios يعيدان كائنًا جاهزًا، فلا تمرّره لـ JSON.parse مرة
ثانية. القاعدة: JSON.parse يأخذ نصًا فقط.
4) 'I' أو 'P' أو أي حرف كبير: رسالة خطأ نصية
Internal Server Error أو Proxy error: Could not proxy request أو
Too Many Requests: الخادم أو الـ proxy ردّ برسالة عادية لا JSON. الحرف الأول من
الرسالة هو ما يظهر لك. الحل نفسه: res.ok أولًا، ثم اقرأ النص لتعرف الرسالة
كاملة.
5) فراغ: Unexpected end of JSON input
الرد بلا جسم: 204 No Content، أو DELETE ناجح لا يعيد شيئًا، أو خادم انهار في
منتصف الرد. افحص قبل التحليل:
if (res.status === 204) return null;
const text = await res.text();
const data = text ? JSON.parse(text) : null;
6) JSON مكسور فعلًا: قواعد أصرم من جافاسكريبت
// صالح في جافاسكريبت، مكسور في JSON
{
name: 'أحمد', // المفاتيح والنصوص: علامة " المزدوجة فقط
age: 17, // الفاصلة الأخيرة ممنوعة
// والتعليقات ممنوعة أيضًا
}
// JSON سليم
{ "name": "أحمد", "age": 17 }
الموضع في الرسالة يدلك على المكان: at position 8 (line 1 column 9) يعني الحرف
التاسع في السطر الأول. وفي ملفات الإعداد (package.json مثلًا) يشير المحرر للسطر
المكسور فورًا.
حرف خفي آخر: BOM، علامة غير مرئية (U+FEFF) يضعها Notepad القديم وبعض
المحررات في أول الملف. الرسالة تبدو غريبة: Unexpected token '' وبين علامتي
الاقتباس حرف موجود لكنك لا تراه.
احفظ الملف بترميز «UTF-8» بلا BOM.
وعلى الخادم: أنشئ الرد بأداة التسلسل الرسمية (JSON.stringify أو res.json()
في Express، وjson.dumps أو JsonResponse في بايثون)، لا بلصق النصوص يدويًا.
نفس الخطأ في بايثون
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
جرّبنا: json.loads("") وjson.loads("<!DOCTYPE html>") يعطيان هذه الرسالة نفسها
بالضبط. مع requests تظهر كـ requests.exceptions.JSONDecodeError عند
response.json(). التشخيص نفسه:
import requests
r = requests.get("https://example.com/api/lessons", timeout=10)
print(r.status_code, r.headers.get("Content-Type"))
print(r.text[:200])
r.raise_for_status() # يرمي خطأ HTTP واضحًا بدل خطأ JSON مضلّل
data = r.json()
وأخطاء الكسر الحقيقي لها رسائل أوضح في بايثون:
Expecting property name enclosed in double quotes للاقتباس المفرد، و
Illegal trailing comma before end of object للفاصلة الزائدة.
قائمة تحقّق سريعة
- اطبع
res.statusوContent-Typeوأول 200 حرف منres.text(). '<'؟ صفحة HTML: الرابط، أو المصادقة، أو 404 مقنّع بـ 200."undefined"؟ قيمة غير موجودة، غالبًاlocalStorage."[object Object]"؟ لا تحلّل كائنًا محلَّلًا.end of JSON input؟ جسم فارغ أو 204.- موضع متقدم؟ JSON مكسور: اقتباسات مزدوجة، ولا فواصل أخيرة، ولا تعليقات.
res.okقبلres.json()في كلfetchتكتبه.
الخلاصة
Unexpected token in JSON رسالة مضلّلة الاسم: نادرًا ما تكون مشكلة JSON، وغالبًا
رد غير متوقع من الخادم التقطه JSON.parse أولًا. اقرأ الحرف، واطبع النص الخام،
وافحص الحالة، وستجد السبب الحقيقي خلال دقيقة.
التعامل مع البيانات من الأساس في درس JSON وfetch، وأخطاء العائلة في أشهر أنواع TypeError وحل Cannot read properties of undefined.