【Axios+Vue错误处理】中后台项目实战:从错误分类到统一捕获,彻底搞定系统/业务/接口异常,避开重复提示与白屏坑!

📑 文章目录
- 一、开篇:为什么要统一处理错误?
- 二、先搞清楚:错误到底分哪几种?
- [2.1 三种错误类型](#2.1 三种错误类型)
- [2.2 系统异常:JS 跑崩了](#2.2 系统异常:JS 跑崩了)
- [2.3 业务异常:业务逻辑告诉你"不行"](#2.3 业务异常:业务逻辑告诉你“不行”)
- [2.4 接口错误:HTTP 或请求层出问题](#2.4 接口错误:HTTP 或请求层出问题)
- 三、错误处理的核心原则
- [四、实战:Axios 请求层封装](#四、实战:Axios 请求层封装)
- [4.1 项目结构建议](#4.1 项目结构建议)
- [4.2 完整请求封装示例](#4.2 完整请求封装示例)
- [4.3 统一错误处理函数](#4.3 统一错误处理函数)
- 五、业务层怎么处理"业务异常"?
- [5.1 接口层统一处理 vs 业务层补充](#5.1 接口层统一处理 vs 业务层补充)
- [5.2 业务层处理示例](#5.2 业务层处理示例)
- [5.3 在 Vue 组件中使用](#5.3 在 Vue 组件中使用)
- 六、全局兜底:系统异常(如白屏)怎么防?
- [6.1 Vue 的 errorHandler](#6.1 Vue 的 errorHandler)
- [6.2 全局未捕获的 Promise 异常](#6.2 全局未捕获的 Promise 异常)
- [6.3 Vue 错误边界组件(Vue 3.4+)](#6.3 Vue 错误边界组件(Vue 3.4+))
- 七、常见踩坑与避坑指南
- [7.1 拦截器里 return 和 Promise.reject 搞混](#7.1 拦截器里 return 和 Promise.reject 搞混)
- [7.2 重复提示用户](#7.2 重复提示用户)
- [7.3 取消请求未处理](#7.3 取消请求未处理)
- [7.4 不同后端约定的 code 不一样](#7.4 不同后端约定的 code 不一样)
- 八、异步请求的推荐写法
- [8.1 async/await + try/catch](#8.1 async/await + try/catch)
- [8.2 并发请求](#8.2 并发请求)
- [8.3 避免 async 没有 catch](#8.3 避免 async 没有 catch)
- 九、小结:一张图帮你记牢
- [🔍 系列模块导航](#🔍 系列模块导航)
- [📝 API 与异步请求规范](#📝 API 与异步请求规范)
- [📚 系列总览](#📚 系列总览)
同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。
(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)
很多前端开发者都会遇到一个瓶颈:
代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。
想写出干净、优雅、可维护 的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验。
这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。
帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。
一、开篇:为什么要统一处理错误?
开发时常见两类人:一类"到处 try-catch",一类"基本不写 try-catch"。前者代码臃肿,后者一旦出错就白屏或控制台一片红。
规范错误处理的目的很简单:让错误有统一的去处,并能用统一、友好的方式提示给用户。
这篇文章会围绕三件事展开:
- 错误分类:系统异常、业务异常、接口错误
- 统一捕获:在哪捕获、怎么捕获、如何上报
- 规范实践:Axios 封装、异步流程、Vue 项目中的落地方式
[⬆ 返回目录](#⬆ 返回目录)
二、先搞清楚:错误到底分哪几种?
2.1 三种错误类型
| 类型 | 举例 | 谁该处理 | 用户需要看到吗 |
|---|---|---|---|
| 系统异常 | JS 报错、网络断开、请求超时 | 全局兜底 | 需要通用提示 |
| 业务异常 | 登录过期、余额不足、无权限 | 业务逻辑 | 需要具体文案 |
| 接口错误 | 4xx、5xx、接口返回错误码 | 请求层统一处理 | 视情况而定 |
[⬆ 返回目录](#⬆ 返回目录)
2.2 系统异常:JS 跑崩了
典型场景:空指针、类型错误、未定义变量等。
js
// 典型的系统异常
const obj = null;
console.log(obj.name); // ❌ Cannot read property 'name' of null
const arr = undefined;
arr.push(1); // ❌ Cannot read property 'push' of undefined
这类错误会直接中断代码执行,需要全局兜底,避免白屏。
[⬆ 返回目录](#⬆ 返回目录)
2.3 业务异常:业务逻辑告诉你"不行"
典型场景:余额不足、登录失效、无权限等,接口一般会返回业务错误码和文案。
js
// 接口返回的业务异常示例
{
"code": 40001,
"message": "余额不足,请先充值",
"data": null
}
这类要由业务层识别并处理,通常要显示具体提示,而不是笼统的"请求失败"。
[⬆ 返回目录](#⬆ 返回目录)
2.4 接口错误:HTTP 或请求层出问题
典型场景:404、500、网络超时、CORS 等。
js
// 常见的接口错误
// 1. 404 - 接口地址不存在
// 2. 500 - 服务端内部错误
// 3. 网络超时 - 用户网络不稳定
// 4. 请求被取消 - 用户快速切换页面导致
这类要在请求封装层统一处理,避免每个接口单独写错误逻辑。
[⬆ 返回目录](#⬆ 返回目录)
三、错误处理的核心原则
记住三条即可:
- 分层处理:全局兜底 + 请求层拦截 + 业务层补充
- 统一入口:错误都经过统一的处理函数,方便打日志、上报
- 用户友好:不直接展示技术报错,而是可读的提示文案
[⬆ 返回目录](#⬆ 返回目录)
四、实战:Axios 请求层封装
4.1 项目结构建议
src/
├── api/
│ ├── request.js # Axios 实例封装
│ ├── modules/ # 按业务模块拆分接口
│ │ ├── user.js
│ │ └── order.js
│ └── interceptors.js # 拦截器(可选,也可以写在 request.js)
├── utils/
│ ├── errorHandler.js # 统一错误处理
│ └── message.js # 统一消息提示
[⬆ 返回目录](#⬆ 返回目录)
4.2 完整请求封装示例
js
// src/api/request.js
import axios from 'axios';
import { showError, showWarning } from '@/utils/message';
import { handleError } from '@/utils/errorHandler';
// 创建 axios 实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 15000,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
request.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器 - 这里是错误处理的核心
request.interceptors.response.use(
(response) => {
const { data } = response;
// 情况1:HTTP 200,但业务上可能失败(很多后端会这样设计)
if (data.code !== undefined && data.code !== 0 && data.code !== 200) {
// 业务异常:交给统一处理
handleError({
type: 'business',
code: data.code,
message: data.message || data.msg || '请求失败',
response: response,
});
return Promise.reject(new Error(data.message || data.msg));
}
// 情况2:业务成功,直接返回 data
return data;
},
(error) => {
// 情况3:HTTP 非 2xx、网络错误、超时等
handleError({
type: 'network',
error: error,
response: error.response,
});
return Promise.reject(error);
}
);
export default request;
[⬆ 返回目录](#⬆ 返回目录)
4.3 统一错误处理函数
js
// src/utils/errorHandler.js
/**
* 统一错误处理入口
* @param {Object} params
* @param {'business' | 'network'} params.type - 错误类型
* @param {number} [params.code] - 业务错误码
* @param {string} [params.message] - 错误信息
* @param {Error} [params.error] - 原始错误对象
* @param {Object} [params.response] - axios 响应对象
*/
export function handleError({ type, code, message, error, response }) {
// 1. 用户提示(选合适的 UI 组件)
const userMessage = getErrorMessage(type, code, message, error, response);
showErrorToUser(userMessage);
// 2. 开发环境打印,方便调试
if (import.meta.env.DEV) {
console.error('[ErrorHandler]', { type, code, message, error, response });
}
// 3. 生产环境上报(对接你们的监控平台)
if (import.meta.env.PROD) {
reportError({ type, code, message, error, response });
}
}
/**
* 根据错误类型和状态码,生成对用户友好的提示
*/
function getErrorMessage(type, code, message, error, response) {
if (type === 'business') {
return message || '操作失败,请稍后重试';
}
if (type === 'network') {
if (!error) return '网络异常,请检查网络后重试';
if (error.code === 'ECONNABORTED') {
return '请求超时,请稍后重试';
}
if (error.message === 'Network Error') {
return '网络连接失败,请检查网络';
}
if (error.message?.includes('canceled')) {
return null; // 请求被取消,一般不提示
}
const status = response?.status;
const statusMap = {
400: '请求参数错误',
401: '登录已过期,请重新登录',
403: '没有权限访问',
404: '请求的资源不存在',
500: '服务器繁忙,请稍后重试',
502: '服务暂时不可用',
503: '服务暂时不可用',
};
return statusMap[status] || message || '请求失败,请稍后重试';
}
return '系统异常,请稍后重试';
}
/**
* 对用户的提示(根据项目用的 UI 库替换)
*/
function showErrorToUser(msg) {
if (!msg) return; // 取消等情况不提示
// 如果用 Element Plus
// import { ElMessage } from 'element-plus';
// ElMessage.error(msg);
// 如果用 Ant Design Vue
// import { message } from 'ant-design-vue';
// message.error(msg);
// 简单示例:用原生 alert 或自定义 toast
console.warn('[User Message]', msg);
}
/**
* 错误上报(对接 Sentry、阿里云 ARMS 等)
*/
function reportError(payload) {
// 示例:可接入 Sentry
// Sentry.captureException(new Error(JSON.stringify(payload)));
console.log('[Report]', payload);
}
[⬆ 返回目录](#⬆ 返回目录)
五、业务层怎么处理"业务异常"?
5.1 接口层统一处理 vs 业务层补充
| 场景 | 建议 | 说明 |
|---|---|---|
| 通用错误(401、500 等) | 拦截器统一处理 | 避免到处写相同逻辑 |
| 特定业务错误(如余额不足) | 业务层单独处理 | 需要特殊逻辑(如弹充值框) |
| 静默失败 | 接口返回,不全局提示 | 例如列表加载失败只影响当前区域 |
[⬆ 返回目录](#⬆ 返回目录)
5.2 业务层处理示例
js
// src/api/modules/order.js
import request from '../request';
/**
* 创建订单
* 业务层可以根据返回的 code 做特殊处理
*/
export function createOrder(data) {
return request({
url: '/order/create',
method: 'post',
data,
}).catch((err) => {
// 某些业务错误希望在调用方处理,可以不在这里 throw
// 但大多数情况下,拦截器已经处理过了,这里只是补充逻辑
if (err.message?.includes('库存不足')) {
// 可以跳转库存页面、弹窗等
console.log('跳转到库存不足提示页');
}
throw err; // 继续向上抛出,让调用方知道失败了
});
}
[⬆ 返回目录](#⬆ 返回目录)
5.3 在 Vue 组件中使用
html
<script setup>
import { ref } from 'vue';
import { createOrder } from '@/api/modules/order';
const loading = ref(false);
const handleSubmit = async () => {
loading.value = true;
try {
const res = await createOrder({ productId: 123, count: 2 });
// 成功后的逻辑
console.log('订单创建成功', res);
// 可以再弹一个成功提示
} catch (err) {
// 错误已经被拦截器处理并提示过了
// 这里可以做:关闭 loading、重置表单、跳转等
console.log('订单创建失败', err);
} finally {
loading.value = false;
}
};
</script>
[⬆ 返回目录](#⬆ 返回目录)
六、全局兜底:系统异常(如白屏)怎么防?
6.1 Vue 的 errorHandler
Vue 3 提供了全局错误处理,适合兜底组件、生命周期、watch 等抛出的错误:
js
// main.js 或 main.ts
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.config.errorHandler = (err, instance, info) => {
console.error('Vue 全局错误:', err);
console.error('错误信息:', info);
console.error('出错组件:', instance);
// 用户提示
// showError('页面出现异常,请刷新重试');
// 上报
// reportError({ type: 'vue', error: err, info });
};
app.mount('#app');
[⬆ 返回目录](#⬆ 返回目录)
6.2 全局未捕获的 Promise 异常
未处理的 Promise.reject 或 async 抛错会变成 unhandledrejection,需要单独监听:
js
// main.js 中追加
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的 Promise 错误:', event.reason);
event.preventDefault(); // 阻止默认控制台报错(可选)
// 用户提示
// showError('操作失败,请稍后重试');
// 上报
// reportError({ type: 'unhandledrejection', reason: event.reason });
});
[⬆ 返回目录](#⬆ 返回目录)
6.3 Vue 错误边界组件(Vue 3.4+)
Vue 3.4 起支持 onErrorCaptured,可以做到局部错误边界,避免整个应用崩溃:
html
<!-- ErrorBoundary.vue -->
<template>
<div v-if="hasError" class="error-fallback">
<p>哎呀,这部分内容加载出错了</p>
<button @click="retry">重试</button>
</div>
<slot v-else />
</template>
<script setup>
import { ref, onErrorCaptured } from 'vue';
const hasError = ref(false);
onErrorCaptured((err) => {
hasError.value = true;
console.error('子组件错误被捕获:', err);
return false; // 阻止继续向上冒泡
});
const retry = () => {
hasError.value = false;
};
</script>
[⬆ 返回目录](#⬆ 返回目录)
七、常见踩坑与避坑指南
7.1 拦截器里 return 和 Promise.reject 搞混
js
// ❌ 错误:业务失败时直接 return,调用方拿不到错误
request.interceptors.response.use((response) => {
if (data.code !== 0) {
showError(data.message);
return; // 危险!调用方 await 会得到 undefined
}
return data;
});
// ✅ 正确:必须 reject,调用方才能在 catch 里处理
request.interceptors.response.use(
(response) => {
const { data } = response;
if (data.code !== 0) {
showError(data.message);
return Promise.reject(new Error(data.message));
}
return data;
},
(error) => Promise.reject(error)
);
[⬆ 返回目录](#⬆ 返回目录)
7.2 重复提示用户
js
// ❌ 拦截器已经 showError,组件里又 showError
// 拦截器
handleError({ message: '登录过期' }); // 弹了一次
// 组件
catch (err) {
showError('登录过期'); // 又弹一次!
}
// ✅ 拦截器统一提示,组件只做业务逻辑(如跳转登录)
catch (err) {
if (err.message?.includes('登录')) {
router.push('/login');
}
}
[⬆ 返回目录](#⬆ 返回目录)
7.3 取消请求未处理
快速切换 Tab 时,前一个请求可能还在进行,需要取消:
js
import axios from 'axios';
// 用 CancelToken 或 AbortController
const controller = new AbortController();
request.get('/api/list', {
signal: controller.signal,
});
// 组件卸载或切换时
onUnmounted(() => {
controller.abort();
});
在拦截器里,对 canceled 错误应避免再弹错误提示,只做静默处理。
[⬆ 返回目录](#⬆ 返回目录)
7.4 不同后端约定的 code 不一样
有的用 0 表示成功,有的用 200,有的用 success: true,建议在 request.js 里抽象一层:
js
// 抽象成配置
const SUCCESS_CODES = [0, 200];
const isSuccess = (code) => SUCCESS_CODES.includes(code);
// 或者从 data 里读
const isSuccess = (data) => data.code === 0 || data.success === true;
[⬆ 返回目录](#⬆ 返回目录)
八、异步请求的推荐写法
8.1 async/await + try/catch
js
async function fetchData() {
try {
const res = await getList();
// 处理 res
} catch (err) {
// 可选:拦截器已提示,这里做业务补充
} finally {
loading.value = false;
}
}
[⬆ 返回目录](#⬆ 返回目录)
8.2 并发请求
js
// ✅ 用 Promise.all,一个失败整体失败
const [users, orders] = await Promise.all([
getUserList(),
getOrderList(),
]);
// 需要"部分成功也算成功"时用 Promise.allSettled
const results = await Promise.allSettled([api1(), api2()]);
[⬆ 返回目录](#⬆ 返回目录)
8.3 避免 async 没有 catch
js
// ❌ 危险
async function init() {
const data = await fetchData(); // 一旦失败,没有 catch
doSomething(data);
}
init();
// ✅ 要么加 catch,要么在调用处包一层
init().catch(handleError);
[⬆ 返回目录](#⬆ 返回目录)
九、小结:一张图帮你记牢
用户操作
↓
组件调用 API
↓
Axios 请求 → 请求拦截器(加 token 等)
↓
服务器响应
↓
响应拦截器
├─ HTTP 2xx + 业务成功 → 返回 data
├─ HTTP 2xx + 业务失败 → handleError(business) → reject
└─ HTTP 非 2xx / 网络错误 → handleError(network) → reject
↓
handleError:提示用户 + 开发日志 + 生产上报
↓
组件 catch:业务逻辑(跳转、重置等)
↓
全局兜底:Vue errorHandler + unhandledrejection
记住三点即可:
- 请求层:统一处理 HTTP 与业务错误,避免到处重复写。
- 业务层:只做特殊逻辑,不要重复弹提示。
- 全局 :用
errorHandler、unhandledrejection兜底,防止白屏和未捕获异常。
[⬆ 返回目录](#⬆ 返回目录)
🔍 系列模块导航
📝 API 与异步请求规范
一、《Axios 统一封装实战:拦截器配置 + baseURL 优化 + 接口规范,避坑重复代码|API 与异步请求规范篇》
二、《Axios 接口请求规范实战:请求参数 / 响应处理 / 异常兜底,避坑中后台 API 调用混乱|API 与异步请求规范篇》
三、《Axios + Vue 错误处理规范:中后台项目实战,统一捕获系统 / 业务 / 接口异常|API 与异步请求规范篇》
四、《前端实战:Excel 导入导出规范(命名 + 校验 + 错误处理 + 统一交互)|API 与异步请求规范篇》
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
📚 系列总览
前端体系化学习完全体:基础 → 规范 → 架构 → 大厂面试
四套系列、百余篇高质量实战文,从入门到进阶,一站式补齐前端核心能力
- 前端基础实战系列 : 《前端基础实战:JS/TS与Vue体系化扫盲(47 篇完整目录 + 避坑)》
- 前端规范实战系列 : 《JS/TS/Vue 前端规范实战:从写对到写优,搞定中后台规范落地,打造可维护代码(40 篇全目录)》
- 前端架构实战系列:聚焦工程化、性能优化、可维护架构、中后台体系设计(持续更新中)
- 前端大厂面试系列:覆盖高频考点、手写题、项目深挖、简历与面试技巧(规划中)
每个系列完结后,都会整理成一篇完整导航文并附上直达链接,方便大家按顺序、体系化学习。
全套内容持续更新中,敬请期待~
[⬆ 返回目录](#⬆ 返回目录)
技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护。
哪怕每次只吃透一条规范,长期下来,差距会非常明显。
后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。
觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。
我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~