前端错误处理:try/catch 与 Promise 的正确姿势
前端项目里,一遇到错误就到处塞 try/catch 的情况挺多见的。但另一方面,真正该处理的地方又给忘了——比如 fetch 的异常判断、JSON 解析的安全兜底。结果线上出了问题不好排查,错误提示要么太技术,要么压根没有。
要把错误处理好,关键就是两件事:知道 try/catch 能管什么、不能管什么,然后把'业务上能预料到的失败'和'真正的系统异常'分开处理。下面结合一些常见场景聊一下。
1. try/catch 到底能抓到什么
很多人的直觉是:try 块里的代码只要报错,catch 就能接住。其实只对了一半。
同步代码没问题
try {
const obj = JSON.parse('{ invalid json }'); // 抛出 SyntaxError
console.log(obj);
} catch (e) {
console.error('解析失败:', e.message); // 能抓到
}
因为 JSON.parse 的异常是同步抛出的,执行还没跳出 try,所以能被 catch 捕获。
异步里的错误,try/catch 就管不着了
try {
setTimeout(() => {
throw new Error('异步报错'); // 这个 catch 抓不到!
}, 0);
} catch (e) {
console.error(e); // 不会执行
}
setTimeout 的回调在下一个事件循环才执行,那时候 try 早就走完了,错误会直接冒泡成未捕获异常。类似的,Promise 内部的 reject、Ajax 回调里的错误也这么任性,得用别的办法对付。
2. fetch 的错误处理比你想的麻烦
fetch 是什么?
简单说,fetch 是浏览器自带的 API,用来向后端发请求、拿数据。它返回一个 Promise,所以用 async/await 配合起来很顺手。
常用姿势:
const res = await fetch('/api/user/list');
const data = await res.json(); // 解析响应体为 JSON
console.log(data);
这里有几个概念:
| 概念 | 解释 |
|---|---|
fetch(url) | 向该地址发起请求(默认 GET) |
| 返回值 | Promise,需要 await |
res | 响应对象,包含状态码、响应体等 |
res.json() | 将响应体解析为 JS 对象 |
res.text() | 获取纯文本响应体 |
一个常见的坑
很多人习惯这样写:
try {
const res = await fetch('/api/user/list');
const data = await res.json();
return data;
} catch (e) {
console.error('请求失败', e);
}
但这有个隐蔽问题:fetch 在收到 4xx、5xx 这些 HTTP 错误状态码时,并不会抛出异常。只有当网络彻底不通、跨域被拦截等底层错误发生时,才会抛出异常。所以上面的代码会把 404、500 当成成功处理,直接跑进 res.json(),可能接着解析到一堆乱七八糟的响应体。
怎么处理才对
要同时兜住网络异常和 HTTP 状态异常,一般这么做:
async function fetchUserList() {
try {
const res = await fetch('/api/user/list');
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json();
return data;
} catch (e) {
if (e.name === 'TypeError' && e.message.includes('fetch')) {
console.error('网络异常,请检查网络');
} else {
console.error('请求失败:', e.message);
}
throw e; // 让调用方也知道失败了
}
}
要点:
- 用
res.ok先判断状态码是不是 2xx;不是就主动抛一个错误 res.json()若遇非法 JSON 也会抛异常,能被同一个 catch 接住- 网络类错误特征比较明显(
TypeError里包含 'fetch'),可以在 catch 里区分一下提示
3. JSON 解析这个小坑,得封个 safeParse
后端返回的东西不一定靠谱:可能直接是个字符串 "用户不存在",也可能结构错乱。前端里直接 JSON.parse,一崩就是一片。
我一般会在项目里放一个 safeParse 工具函数:
function safeParse(str, fallback = null) {
try {
return JSON.parse(str);
} catch (e) {
console.warn('JSON 解析失败:', e.message, '原始内容:', str?.slice(0, 50));
return fallback;
}
}
const data = safeParse(response, {});
这样解析失败时不会让整个页面挂掉,而是静默回到一个兜底值(空对象或空数组),同时控制台里打一条 warn,方便查问题。
4. 业务异常和系统异常,分开处理
如果所有错误都不加区分地用 catch 糊成一团,用户界面上可能看到'网络异常'但其实是余额不足,或者技术错误细节直接暴露。所以得把两类错误分开。
业务异常——那些可以预料到的'业务上的失败':余额不足、未登录、参数错误等等。后端通常会通过业务码(如 code: 'BALANCE_INSUFFICIENT')和人类可读的消息返回。这类错误要明确提示用户,让用户知道下一步该做什么。
系统异常——网络断了、服务器 500、JSON 解析炸了、未知的运行时错误。这些不该把细节暴露给用户,应该上报监控,用户只看到一个通用的'系统繁忙'之类的提示。
下面是一个下单接口的例子,同时处理这两类:
async function placeOrder(orderData) {
try {
const res = await fetch('/api/order/create', {
method: 'POST',
body: JSON.stringify(orderData),
});
const text = await res.text();
const data = safeParse(text, null);
if (!res.ok) {
// 业务异常:有明确的业务码
if (data?.code === 'BALANCE_INSUFFICIENT') {
return { success: false, type: 'business', message: '余额不足,请先充值' };
}
if (data?.code === 'UNAUTHORIZED') {
return { success: false, type: 'auth', message: '请先登录' };
}
// 其他 4xx/5xx 当成系统异常往上抛
return { success: false, type: 'system', message: data?.message || '服务器异常,请稍后重试' };
}
return { success: true, data };
} catch (e) {
// 系统异常:网络错误、JSON 解析异常等
reportError(e);
return { success: false, type: 'system', message: '网络异常,请检查网络后重试' };
}
}
调用方就能清晰地分开提示:
const result = await placeOrder(formData);
if (result.success) {
// 跳转成功页
} else if (result.type === 'business' || result.type === 'auth') {
message.warning(result.message); // 可操作的业务提示
} else {
message.error(result.message); // 系统异常,建议稍后重试
}
5. 几个实用的处理规范
- 该用 try/catch 的地方:JSON.parse、可能抛异常的第三方库、同步业务校验逻辑
- 别指望 try/catch 的地方:异步回调、事件监听器里的错误。这些要用 Promise 的 .catch(),或者 async/await 搭配 try/catch 包住 await 那一行
- 全局兜底:别忘了挂一个
window.onerror或监听unhandledrejection,来捕获那些漏网之鱼
下面把上面提到的 safeParse 和错误分类思路整合成一个带错误上报的产品详情接口:
async function getProductDetail(id) {
try {
const res = await fetch(`/api/product/${id}`);
const text = await res.text();
const data = safeParse(text);
if (!res.ok) {
if (data?.code === 'NOT_FOUND') {
return { ok: false, reason: 'product_not_found' };
}
throw new Error(data?.message || `请求失败:${res.status}`);
}
return { ok: true, data };
} catch (e) {
if (e.name === 'SyntaxError') {
reportError(e, { context: 'JSON 解析', id });
return { ok: false, reason: 'parse_error' };
}
if (e.message?.includes('fetch') || e.message?.includes('Network')) {
return { ok: false, reason: 'network_error' };
}
throw e; // 未知错误继续向上抛,让全局兜底处理
}
}
最后
前端错误处理没有一招鲜,关键是把'预期的业务失败'和'真正的意外异常'拆开,然后在合适的地方用合适的工具。下面这张表可以当速查用:
| 错误类型 | 处理方式 | 注意点 |
|---|---|---|
| Ajax 网络错误 | try/catch + res.ok 判断 | 4xx/5xx 不会自动抛异常 |
| JSON 解析错误 | 对 JSON.parse 包 try/catch | 封装 safeParse 复用 |
| 业务异常 | 根据 code 分支,返回固定结构 | 给用户明确提示 |
| 系统异常 | catch 后上报 + 通用提示 | 避免暴露内部细节 |
| 异步错误 | Promise .catch / async try | 不要指望外层同步 try 捕获 |
记这几点,线上排查和用户体验都能轻松不少。


