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,或者用二分法截取前半段单独解析,第一段解析失败的边界就是真实出错点。