☰
JSON解析报错排查:从空字符串到undefined的完整解决指南
2026/10/1 23:59:32 网站建设 项目流程

先说实话,这类报错我在日常开发里见得实在太多了,尤其是前端对接接口、处理本地缓存、解析后端返回数据的时候。你大概率遇到过这样的情况:代码明明看着没问题,接口也返回了,但控制台突然抛出一个SyntaxError: Unexpected end of JSON input或者Unexpected token u in JSON at position 0,页面直接白屏,数据也渲染不出来。

这两个报错表面上看是JSON解析失败,但背后对应的场景完全不同。一个意味着你拿到的是“空字符串”,另一个意味着你拿到的是“undefined”这个值。很多人在网上搜了一圈,试了各种方法,最后发现还是搞不清楚根源在哪。这篇文章我一次性把这些坑讲透:两个报错各自的成因、真正的排查思路、怎么从根上避免,以及遇到一系列类似报错时该怎么快速定位。


1. 两个报错到底在说什么

1.1 Unexpected end of JSON input 的真正含义

Unexpected end of JSON input,翻译成大白话就是:JSON解析还没结束,输入就没了。明明需要一段完整的JSON,结果你给了一个空字符串"",或者字符串只写了一半,解析器读到最后也没找到可以收尾的内容,于是直接抛错。

举个例子,你在浏览器控制台里敲一行代码:

JSON.parse("")

马上就会看到:

SyntaxError: Unexpected end of JSON input

原因很简单:JSON.parse期望接收一个字符串,结果这个字符串的长度是0,啥也解析不出来。这就像你去自动售货机买东西,投了币,结果货架是空的,售货机只能吐出一句“货物不存在”。

那实际开发中,什么场景会传给JSON.parse一个空字符串?最常见的是用fetch请求接口时,直接把response.text()的结果丢给JSON.parse。如果后端返回了一个空body(比如204 No Content、302重定向后的空页面、网关超时返回的空响应),那么text()拿到的就是"",再一parse,必炸。

还有一个容易被忽略的场景:某些后端框架在返回数据时,如果实体类是void或者返回值为null,序列化后就是空字符串。前端这边不判断就直接JSON.parse,毫无意外地报这个错。

1.2 Unexpected token u 的“u”到底是谁

Unexpected token u in JSON at position 0这个报错,很多人一开始看不太懂:“u是哪来的?”这里的“u”其实是undefined的首字母。

也就是说,你传给JSON.parse的东西不是字符串,而是undefined。JSON.parse内部会尝试把参数转成字符串,undefined被转成字符串"undefined",解析器读第一个字符u,发现JSON里根本不允许出现这个字符,于是抛出Unexpected token u。

测试一下就很直观:

JSON.parse(undefined)

输出:

SyntaxError: Unexpected token u in JSON at position 0

那开发里什么时候会把undefined传给JSON.parse?我列几个典型场景:

  • 从localStorage里取值,键不存在时getItem返回null,但如果你再做了一层处理,比如JSON.parse(storageData || undefined),就会传入undefined
  • 某个变量没初始化,直接JSON.parse(someVariable),而someVariable的值是undefined
  • 后端返回的字段不存在,前端拿到undefined,又没做兜底就丢给JSON.parse

这里有个特别容易记混的点:JSON.parse(null)其实不报错,它返回null;但JSON.parse(undefined)会直接崩溃。因为null转成字符串是"null",是合法JSON;undefined转成字符串是"undefined",不是合法JSON。

注意:这两个报错看似都是SyntaxError,处理方式却完全不同。一个要查“为什么返回了空字符串”,另一个要查“为什么值是undefined”。别用同一个套路去修。


2. 定位问题:先确认坏数据是从哪个环节来的

2.1 别把报错当成语法错误,当成“字符串中毒”来排查

我见过不少新手一看到SyntaxError,就开始研究JSON语法规范,觉得自己写的JSON格式不对。实际上,绝大多数这类报错压根不是你写的JSON字符串有问题,而是你传进去的那个字符串本身就不是JSON。

更准确的思路是:把这个报错当成“某种非预期字符串进入了JSON.parse”。你可以把整个链路拆开:

  1. 数据是从哪来的?(接口返回、本地存储、用户输入、配置文件)
  2. 这个数据经过哪些转换?(JSON.stringify、模板拼接、编码解码)
  3. 最终传给JSON.parse的字符串是什么?

定位问题最直接的方式,就是在报错之前把参数打出来。别偷懒,加一行console.log看它到底长什么样:

const rawData = await response.text(); console.log("rawData:", JSON.stringify(rawData)); // 打印出来看 const data = JSON.parse(rawData);

这里我故意用JSON.stringify包一下再打印,目的是让你看到字符串里有没有隐藏的换行、空格、空字符串。如果你直接打印rawData,控制台里可能显示得很正常,实际上是空字符串,肉眼不容易分辨。

还有一个更简单的判断方法:直接在浏览器控制台里把变量复制过来,手动执行JSON.parse,看报什么错。如果手动parse也报同样的错,那就说明数据源有问题,而不是代码逻辑有问题。

2.2 前端最常见的五个数据来源坑

我在项目里排查过大量类似问题,总结下来,前端的数据来源有五个高频坑:

接口返回空body

有些接口设计得比较粗糙,比如删除操作返回204、更新操作返回200但body为空。你用response.json()解析时,浏览器自己会报Unexpected end of JSON input;如果用response.text()再parse,同样逃不掉。

本地存储的值不存在

从localStorage或sessionStorage中读数据,键不存在时返回null。如果你直接用JSON.parse(localStorage.getItem("userInfo")),null虽然不会报错,但后续你拿userInfo.name就会报错,因为userInfo是null。更糟糕的是,如果你在读取后还加了一层“默认值”处理,反而可能把null变成undefined,直接触发Unexpected token u。

跨域代理返回了HTML

调试时走webpack代理或者nginx代理,后端挂了,代理返回一个502页面,内容是HTML。你拿response.text()一看,开头是<!DOCTYPE html>,JSON.parse解析第一个字符<,报错就变成了Unexpected token < in JSON at position 0。这本质上和Unexpected token u是一类问题。

对象被转成了字符串

很多人在把对象存到本地时,没有用JSON.stringify,而是直接String(obj)或者用模板字符串${obj},结果存进去的是[object Object]。解析的时候JSON.parse("[object Object]"),第一个字符[理论上合法,但读到后面object就报错了,通常是Unexpected token o in JSON at position 1。

后端返回了null而不是对象

有些接口在无数据时返回{"data": null},前端没判断,直接拿result.data丢给JSON.parse。如果result.data是null,那还不至于报错,但如果是result.data本身不存在,拿到的就是undefined,直接触发Unexpected token u。

2.3 一个可复用的排查清单

遇到这类报错,我建议你按下面的清单逐项排查,基本能覆盖90%的情况:

排查项操作方法常见结果
在网络面板看响应体F12打开Network,找到对应请求,看Response标签页响应体为空、是HTML、被截断
查看响应状态码看HTTP Status Code204、304、500、502等情况
检查Content-Type确认返回的是application/json返回text/html时,八成不是JSON
打印原始字符串在JSON.parse前加日志输出空字符串、undefined、null、一堆HTML
检查调用链传参往上追一层,看传给parse的变量是否有初始值某个变量在异步回调里还没赋值

这套排查方法我用了很久,核心思路就是“先看数据,再改代码”。很多时候,你不需要立刻改逻辑,只需要确认数据到底是什么,问题就已经解决一半了。


3. 实战:给fetch包一层安全的JSON解析

3.1 基础版:先text()再parse

很多人习惯直接response.json(),但response.json()内部就是调用response.text()再JSON.parse。它的问题在于,一旦响应体不是合法JSON,报错信息不够友好,而且你无法得知原始内容。

我认为更好的习惯是:先text(),拿到原始字符串,判断非空后再JSON.parse。这样可控性高很多:

async function request(url, options = {}) { const response = await fetch(url, options); // 非2xx状态码直接抛出,避免拿错误页面去parse if (!response.ok) { throw new Error(`HTTP error: ${response.status} ${response.statusText}`); } const text = await response.text(); if (!text) { // 响应体为空,根据业务决定返回什么 return null; } // 尝试解析,失败时抛出清晰错误 try { return JSON.parse(text); } catch (e) { console.error("JSON解析失败,原始内容:", text.slice(0, 200)); throw new Error("接口返回数据格式异常"); } }

这段代码虽然基础,但已经能处理大部分崩溃场景。关键在于:先判断text是否为空,再解析;解析失败时打印出原始内容的前200个字符,方便你快速定位问题。

3.2 进阶版:状态码、超时、Content-Type全处理

基础版解决了“空字符串”和“解析失败”问题,但还不够。实际项目中,你还会遇到:请求超时返回了错误页面、后端返回了非JSON格式的错误信息、接口状态码是200但内容却是HTML。

我通常会在封装里再补三件事:

async function request(url, options = {}, timeout = 10000) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { ...options, signal: controller.signal, }); const contentType = response.headers.get("Content-Type") || ""; // 防抖:如果返回的不是JSON,直接当成异常处理 if (!contentType.includes("application/json")) { throw new Error("接口Content-Type不是application/json"); } const text = await response.text(); if (!text) { return null; } // 防止拿到类似"undefined"这种字符串 if (text === "undefined" || text === "null") { return null; } try { return JSON.parse(text); } catch (e) { console.error("JSON解析异常,响应文本前100字符:", text.slice(0, 100)); throw e; } } catch (e) { if (e.name === "AbortError") { throw new Error("请求超时"); } throw e; } finally { clearTimeout(timer); } }

这里有几个细节值得说明:

  • AbortController用来控制超时,防止接口一直挂起,页面卡死
  • Content-Type检查很重要,因为代理或网关出错时,返回的往往是text/html,这种情况早点抛出,比解析后报个莫名其妙的SyntaxError要直观得多
  • 对"undefined"和"null"字符串做显式判断,这两个值即使能通过JSON.parse(比如"null"),返回的null也可能不是你想要的数据,提前拦截更安全

3.3 本地存储与状态管理的防御写法

本地存储是另一个重灾区。我见过太多代码写成这样:

const user = JSON.parse(localStorage.getItem("user"));

如果键不存在,getItem返回null,JSON.parse(null)虽然不报错,但user就变成null,后面访问user.name照样白屏。如果代码里再包一层localStorage.getItem("user") || undefined,那就直接触发Unexpected token u。

我建议封装一个带默认值的安全读取函数:

function safeGetFromStorage(key, defaultValue = null) { try { const value = localStorage.getItem(key); if (!value) { return defaultValue; } return JSON.parse(value); } catch (e) { console.warn(`读取本地存储${key}失败,使用默认值:`, e); return defaultValue; } } // 使用 const user = safeGetFromStorage("user", {}); console.log(user.name); // 不会报错

这套写法有几个好处:

  • 用try/catch兜底,就算存进去的是坏数据,也不会让页面崩溃
  • 返回默认值而不是null或undefined,后续逻辑更安全
  • 读取失败时打印警告,方便发现脏数据

写入端同样要注意,别直接把对象塞进去:

const user = { name: "张三", age: 18 }; localStorage.setItem("user", JSON.stringify(user));

有几次我看到有人图省事,直接localStorage.setItem("user", user),存进去的其实是"[object Object]"。下次读取时,JSON.parse("[object Object]")就会在position 1处报错。这个坑虽然不在标题里,但和标题是同一个家族的问题,顺手一起说了。


4. 其他高频JSON报错速查

Unexpected end of JSON input和Unexpected token u只是JSON解析报错里最常见的两个,实际开发中还有几个高频变体。我把它们整理成一个速查表,方便你遇到类似问题时对照。

报错信息本质原因典型场景解决思路
Unexpected token < in JSON at position 0字符串以<开头代理/网关返回HTML错误页检查response状态码,检查是否走到了代理
Unexpected token o in JSON at position 1字符串是[object Object]对象被直接拼成了字符串使用JSON.stringify序列化
Unexpected token e in JSON at position 1字符串是undefined的变体变量未定义就传入parse兜底默认值,确保参数不为undefined
Unterminated string in JSON at position 8192字符串不完整,末尾缺了引号大JSON被截断、网络传输中断检查响应是否完整,必要时做长度校验
Failed to deserialize the JSON body into the target type后端反序列化失败请求JSON字段类型与后端DTO不匹配比对接口文档,检查字段类型、拼写
TypeError: Object of type set is not JSON serializable后端序列化set失败Python的set类型无法直接转JSON将set转list再序列化
SyntaxError: Invalid or unexpected token字符串含有非法字符混入了中文引号、特殊控制字符检查原始数据,清理非法字符

这一类问题的核心原理是相通的:JSON.parse之前,先确认你给它的到底是个什么东西。

我再展开几个比较典型的场景。

4.1 后端返回的JSON字符串被截断

有个不太常见但很难排查的报错:Unterminated string in JSON at position 8192。这个报错通常出现在JSON字符串比较大的时候,比如接口返回了一长串数据。如果传输中途被断开、网关做了body截断、或者日志系统打印时截断了,JSON.parse读到一半发现字符串没有闭合引号,就会报Unterminated string。

排查这类问题时,我会先在Network面板看响应体大小。如果响应体大小明显比正常值小,基本可以断定是截断。还有一种情况是:服务端使用了流式返回,但客户端读取不完整,这时需要检查有没有错误地中断了流。

另外,如果字符串本身包含大量转义字符,比如SQL日志、代码片段,用JSON.stringify序列化时会把换行符转成\n。如果转义不彻底,就会出现引号错位,同样会导致截断报错。稳妥的做法是:在服务端用标准的json.dumps或JSON.stringify,不要自己手工拼JSON字符串。

4.2 Python/Node/后端场景的坑

不要以为这类报错只属于前端。后端也会遇到。

Python里最常见的坑是TypeError: Object of type set is not JSON serializable。Python的set类型不能直接序列化成JSON,但很多人会把数据库查询结果的一部分直接丢给json.dumps,一旦里面有set,就会报错。解决办法是先转成list:

import json data = {"tags": {"a", "b", "c"}} # set json.dumps(data) # TypeError # 修复 data["tags"] = list(data["tags"]) json.dumps(data) # 正常

Node端也有类似问题。当你处理Map、BigInt时,JSON.stringify同样会出错或丢失数据。JSON.stringify遇到BigInt会直接抛TypeError: Do not know how to serialize a BigInt,遇到Map会序列化成{}。

如果你要支持这些类型,通常会写一个replacer函数,或者在序列化前先转成普通对象。不过最省事的方案是:在接口层统一约定数据格式,避免把Map、Set、BigInt直接暴露给JSON。

4.3 格式化工具与离线排查

排查JSON问题时,一个好用的格式化工具能省不少事。VS Code内置的JSON格式化已经很强了,选中代码,按Shift + Alt + F就能排版。Notepad++可以通过插件实现离线格式化,适合在内网环境工作的人。

命令行的话,我有两个顺手工具:

  • jq:不仅是格式化工具,还能做JSON查询、校验。用jq .就能格式化;用jq empty file.json可以校验文件是否合法
  • Node脚本:如果你不想装额外工具,可以直接用Node的内置能力:
node -e "JSON.parse(require('fs').readFileSync('data.json', 'utf8')); console.log('JSON is valid')"

这个命令在CI脚本里也很实用,能在发布前校验配置文件。


5. 从根上减少JSON报错:写一个统一的安全解析工具

5.1 safeParse工具函数的最佳实践

排查再多,不如提前预防。我在团队里推行了一段时间后,效果明显,现在把方案分享出来。

思路很简单:不要在所有地方直接调用JSON.parse,而是统一走一个safeParse函数。这个函数内部处理所有边界情况,返回一个可预期的结果。

function safeParse(input, fallback = null) { // 1. 处理非字符串输入 if (typeof input !== "string") { return fallback; } // 2. 处理空字符串 if (input.trim() === "") { return fallback; } // 3. 处理特殊字符串 if (input.trim() === "undefined" || input.trim() === "null") { return fallback; } // 4. 真正的JSON解析 try { return JSON.parse(input); } catch (e) { const preview = input.length > 100 ? input.slice(0, 100) + "..." : input; console.warn(`JSON.parse失败,内容预览: ${preview}`, e); return fallback; } }

这个函数有几个特点:

  • 参数不是字符串时,直接返回兜底值,不会让undefined进入parse
  • 空字符串、"undefined"、"null"都在parse前拦截
  • 解析失败时打印内容预览,方便定位
  • 始终返回一个可预期的值,不会因为异常导致页面白屏

有了这个函数,代码就变成了:

const data = safeParse(responseText, []); const config = safeParse(localStorage.getItem("config"), {});

即使上游有问题,也不会再出现Unexpected end of JSON input这种直接摧毁页面的报错。

5.2 在接口层约定和监控

工具函数能解决局部问题,但要从根上减少报错,接口层的约定更重要。

我在团队里要求后端接口统一返回这样的结构:

{ "code": 0, "message": "success", "data": {} }

无论成功失败,data字段一定存在,只是值不同。前端这边,先判断code,再取data,所有解析都统一走safeParse。这样即使后端返回异常,前端也有明确的分支处理,不至于一路炸到白屏。

监控方面,我会在前端捕获解析失败的情况,把原始响应文本的前200个字符上传到日志服务。不要小看这200个字符,它能帮你快速定位是网关问题、后端问题还是代码问题。

5.3 别忽视“看不见的字符”:空格、BOM、中文引号

最后讲一个容易被忽视的细节:JSON字符串中混入了“看不见的字符”。

  • BOM(Byte Order Mark):有些编辑器(尤其是Windows上的记事本)保存文件时会在开头加一个\uFEFF。如果你的JSON文件开头带BOM,JSON.parse会报Unexpected token。解决办法是用编辑器将文件转成UTF-8 without BOM格式。
  • 中文引号:有人从Word或公众号文章里复制JSON,里面的引号被替换成了中文全角引号“”,JSON.parse一律不认。这个只能靠肉眼或正则清理。
  • 不可见空白字符:比如\u00a0(不间断空格),有时候从HTML里复制配置到代码里,会带上这种字符。让JSON.parse解析时,通常会在某个奇怪的位置报Unexpected token。

处理这类问题,我一般会用正则把可见范围内的非法字符全部清掉:

const cleaned = raw.replace(/[\u0000-\u001f\u007f-\u009f\u00a0\u2028\u2029]/g, "");

但注意,这个操作会改变原始内容,最好只在预览调试时使用,不要在生产环境无脑清理。


我个人在实际排查中最大的感受是:这两个报错看起来像是“语法错误”,实际上几乎都是“数据流问题”。只要你肯花三分钟把报错之前的原始字符串打出来看一眼,大部分问题都能当场定位。

真正让我头疼的不是报错本身,而是很多同学遇到报错第一反应就是去翻JSON语法规范,或者百度复制一段代码,结果改了一通也没解决。希望你读完这篇之后,能换个思路:先看数据,再改代码。

最后分享一个好用的小技巧:调试时可以在safeParse里把fallback故意设成一个带标记的值,比如Symbol("fallback"),这样一旦走了兜底分支,你就能立刻在后续逻辑里察觉到数据有问题,而不是等到页面渲染时报一个莫名其妙的其他错误。这个方法帮我抓出过好几个隐藏的数据异常。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询