JSON 報錯資訊反查表:從報錯原文直接定位改法
結論:按 JSON.parse 報錯原文反查原因與改法:12 條高頻報錯對應 Chrome、Firefox、Safari 三種措辭,涵蓋 HTML 錯誤頁、截斷輸入、重複解析、NDJSON 拼接等線上場景,並解釋報錯位置為什麼常指向「下游」。

結論:看到 JSON.parse 報錯先抓位置和 token 兩個事實,位置是 0 就查 HTML 回應、BOM 與傳錯型別,報 Expected 就看前一個字元,不要從報錯位置開始逐行重讀。
報錯原文不是雜訊,是答案:解析器告訴你它在哪個字元、期待什麼、實際收到什麼。把報錯當檢索詞去查,比把整段 JSON 重寫一遍快得多。
JSON.parse() 的報錯資訊由三個部分組成:位置(position 或 line/column)、期待(Expected...)、實際收到(Unexpected token X 或 xxx is not valid JSON)。三個引擎措辭不同——Chrome 和 Node(V8)報 Expected ',' or '}' after property value in JSON at position 25,Firefox 報 JSON.parse: expected ',' or '}' after property value in object at line 3 column 1,Safari 報 JSON Parse error: Expected ',' or '}'——但指的都是同一處語法斷點。所以查報錯時別管措辭差異,先抓三個事實:哪個字元、期待什麼、收到什麼。
一、12 條高頻報錯反查表
下表左邊一列是你在主控台裡看到的最有辨識度的片段(以 V8 措辭為準),右邊是真實原因和改法。Firefox/Safari 的措辭差異放在括號裡。
| 報錯原文(V8) | 真實原因 | 怎麼改 |
|---|---|---|
Unexpected token < in JSON at position 0 |
介面回傳的是 HTML(404 頁、500 錯誤頁、登入跳轉、代理兜底頁),不是 JSON | 先看 response.status 與 Content-Type,別去修 JSON |
Unexpected end of JSON input |
輸入為空或被截斷:空回應體、204、下載中斷、複製漏了最後一行 | 先判空再解析,比對 Content-Length 找截斷點 |
Unexpected token o / "[object Object]" is not valid JSON |
把已解析的物件又傳給 JSON.parse(),物件被強轉成字串 |
typeof 檢查,已是物件就直接用 |
"undefined" is not valid JSON |
變數沒賦值(JSON.parse(undefined)) |
初始化兜底值,解析前判空 |
Expected double-quoted property name in JSON |
尾逗號({"a": 1,}),解析器讀到逗號後期待下一個鍵卻遇到 } |
刪除最後一項後面的逗號 |
Expected property name or '}' in JSON |
鍵沒加引號或用了單引號({name: "Ada"}、{'name': 'Ada'}) |
所有鍵補雙引號 |
Expected ',' or '}' after property value in JSON |
缺逗號({"a": 1 "b": 2})或值後面出現了註解 |
補逗號;刪掉註解 |
Unexpected token '/' |
JSON 裡寫了 // 或 /* */ 註解 |
刪註解,或設定檔改用 JSONC 解析器 |
Bad control character in string literal |
字串裡有裸換行、裸 Tab 或其他 U+0000–U+001F 控制字元 | 換行寫成 \n,改用 JSON.stringify 產生 |
Unterminated string in JSON |
字串開了引號沒閉合,或字串內部的雙引號沒轉義 | 內部引號寫 \" |
Unexpected non-whitespace character after JSON |
一段文字裡有兩個文件:{}{}、NDJSON、JSONP 包裹 |
按行拆開逐行解析,或用 NDJSON 解析器 |
JSON.parse is not a function |
自己宣告的變數把全域 JSON 遮蔽了 |
重新命名變數,別佔用 JSON |
前兩條值得單獨展開,因為它們都不是「JSON 寫錯了」,而是「你拿到的根本不是你以為的東西」。
二、position 0 的三種常見來源
報錯位置是 0,說明第一個字元就非法。你看不到可疑字元,不代表字元不存在——大概率是這三種之一:
第一種:<,回傳了 HTML。 這是線上環境最高頻的解析報錯。介面路徑寫錯、登入態過期被閘道重定向、後端異常渲染了錯誤範本,回應體以 <!DOCTYPE html> 開頭,解析器第一個字元就放棄了。正確動作是打開 Network 面板看原始回應體,把介面問題解決掉,而不是反覆檢查自己的 JSON 語法。
第二種:BOM 或零寬字元。 Windows 記事本儲存的檔案常帶 UTF-8 BOM(\uFEFF),從網頁或聊天工具複製的文字可能帶零寬空格(\u200B–\u200D)。這些字元在編輯器裡不可見,解析器卻看得一清二楚。處理辦法:
// 去掉 BOM 和零寬字元後再解析
const cleaned = raw.replace(/^\uFEFF/, '').replace(/[\u200B-\u200D]/g, '');
const data = JSON.parse(cleaned);
第三種:傳錯了型別。 JSON.parse(undefined) 報 "undefined" is not valid JSON,JSON.parse(someObject) 報 Unexpected token o(物件被強轉成 "[object Object]" 後,o 在 position 1 非法)。這兩種都是「資料已經解析過了」或「根本沒拿到資料」,修法是檢查型別而不是檢查語法:
let data = text;
if (typeof data === 'string') {
data = JSON.parse(data); // 只在確實是字串時才解析
}
三、報錯位置在下游,真實原因在上游
兩類報錯的位置資訊會「騙人」,這也是反查表裡最容易被誤導的兩種情況:
- 缺逗號:
{"a": 1 "b": 2}報Expected ',' or '}' after property value,位置指向"b"那一行——報錯沒錯,但容易被理解成"b"有問題。實際要改的是上一行末尾。 - 缺閉合括號:深層巢狀少了
}或],解析器會一路吞到輸入末尾才報Unexpected end of JSON input。位置在檔案末尾,真正缺括號的地方在報錯位置之前很遠處。往回找最近一個未閉合的{或[,配對計數比目測快。
處理這類問題的通用手法是二分:把報錯位置之前的文字截出來單獨解析,能過就說明問題在報錯位置附近;不能過就繼續往前二分,第一段解析失敗的邊界就是真實出錯點。大檔案不建議肉眼數括號,先格式化,層級對齊之後缺哪一層一眼可見(可以用站內的 JSON 格式化 先整理再查)。
四、解析成功但結果不對的四種「假健康」
比報錯更隱蔽的是不報錯但資料不對。這四種情況 JSON 合法、解析成功,問題出在資料本身的形態:
- 雙重編碼:整個文件被
JSON.stringify了兩次,解析第一層得到的是字串而不是物件。typeof result === 'string'就再 parse 一次,但根治方法是找到重複序列化的源頭。 - 重複鍵靜默覆蓋:
{"name": "Tom", "name": "Jerry"}是合法 JSON,多數解析器不報錯,後者覆蓋前者。資料從多個來源合併時最容易踩,用 JSON 驗證 工具或嚴格的解析選項提前暴露。 - NDJSON 被當單個文件:串流介面每行一個物件,直接 parse 整段文字報
Unexpected non-whitespace character after JSON。按\n切分逐行解析。 - 數字精度悄悄丟失:超過
2^53 - 1的整數(如雪花 ID)解析後末位變成 0,不報任何錯。ID 一律用字串傳輸。
這四種情況的共同點是:解析器認為一切正常。所以介面資料異常時,第一步是列印原始文字的前 80 個字元和 typeof,確認形態,再談內容。
五、寫程式碼時就讓報錯失去機會
反查表是事後手段,預防是事前手段。四個習慣能讓大部分報錯在發生前就被攔截:
// 1. 解析回應前先做兩道檢查
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error(`HTTP ${response.status},介面沒有回傳資料`);
}
const contentType = response.headers.get('content-type') ?? '';
if (!contentType.includes('application/json')) {
throw new Error(`期望 JSON,實際是 ${contentType}`);
}
const data = await response.json(); // 2. json() 內部已處理解析,別再 parse 一次
- 產生用序列化,不用拼接:
JSON.stringify()會正確處理引號、換行、控制字元;手工拼字串是Bad control character和Unterminated string的最大來源。 - try/catch 必須有兜底:
JSON.parse一拋例外就是同步中斷,外層不接住整個流程就掛。解析 localStorage、讀取外部檔案這類輸入不可信的場景,catch 裡要有預設值或明確的錯誤上報。 - 結構驗證交給 Schema:語法合法不等於結構正確,欄位缺失、型別不符要靠 JSON Schema 在邊界處攔截,相關取捨見 JSON Schema 能解決什麼問題。
六、快速決策路徑
最後把全文壓縮成一個決策順序:看到報錯先抓位置和 token——
- 位置是 0 → 查
<(HTML)、BOM、傳錯型別三種來源; - 報
end of input→ 查空回應和截斷,往回找未閉合括號; - 報
Expected ...→ 看前一個字元,八成是缺逗號或缺引號; - 報
after JSON data→ 是多文件拼接,按行拆; - 解析成功但資料怪 → 走「假健康」四項檢查。
配合逐條排查的完整流程(見 JSON 解析報錯怎麼修),90% 的解析問題可以在五分鐘內定位。報錯原文本身值得收藏一份——它是解析器寫給你的除錯日誌,措辭三年不變,查一次管一年。
JSON 報錯 Unexpected token 是什麼意思?
解析器在某個位置遇到了不符合 JSON 語法的字元。token 就是被拒收的那個字元:< 通常是介面回傳了 HTML,' 是用了單引號,/ 是寫了註解,普通字母常見於未加引號的鍵名或 undefined、NaN 這類 JavaScript 專屬字面量。先看 token 是什麼,再看位置,就能把方向收窄到一種原因。
Unexpected end of JSON input 怎麼修?
兩種來源:輸入為空(空回應體、localStorage 裡存了空字串)或輸入被截斷(下載中斷、複製漏行、長度限制)。先 if (!text) return 處理空輸入;再確認文字完整,截斷的往回找最近一個未閉合的 {、[ 或 " 補上。注意這個報錯也可能因為深層缺括號,位置只是恰好落在末尾。
為什麼 Chrome、Firefox、Safari 的報錯不一樣?
三個瀏覽器用不同的 JS 引擎(V8、SpiderMonkey、JavaScriptCore),報錯措辭各自實作,MDN 收錄了全部官方措辭清單。但三者的報錯都包含同樣的三要素——位置、期待、實際收到——語義一一對應,按本表反查時先轉換成 V8 措辭即可。
報錯位置指向的地方看起來沒問題怎麼辦?
說明真實錯誤在報錯位置之前。典型是缺逗號(報錯在下一行)和缺閉合括號(報錯在檔案末尾)。從報錯位置往回逐字元看前一個 token,或者用二分法截取前半段單獨解析,第一段解析失敗的邊界就是真實出錯點。