Axios + Vue 错误处理规范:中后台项目实战,统一捕获系统 / 业务 / 接口异常|API 与异步请求规范篇

【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"。前者代码臃肿,后者一旦出错就白屏或控制台一片红。

规范错误处理的目的很简单:让错误有统一的去处,并能用统一、友好的方式提示给用户。

这篇文章会围绕三件事展开:

  1. 错误分类:系统异常、业务异常、接口错误
  2. 统一捕获:在哪捕获、怎么捕获、如何上报
  3. 规范实践: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. 请求被取消 - 用户快速切换页面导致

这类要在请求封装层统一处理,避免每个接口单独写错误逻辑。

[⬆ 返回目录](#⬆ 返回目录)

三、错误处理的核心原则

记住三条即可:

  1. 分层处理:全局兜底 + 请求层拦截 + 业务层补充
  2. 统一入口:错误都经过统一的处理函数,方便打日志、上报
  3. 用户友好:不直接展示技术报错,而是可读的提示文案

[⬆ 返回目录](#⬆ 返回目录)

四、实战: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

记住三点即可:

  1. 请求层:统一处理 HTTP 与业务错误,避免到处重复写。
  2. 业务层:只做特殊逻辑,不要重复弹提示。
  3. 全局 :用 errorHandler、unhandledrejection 兜底,防止白屏和未捕获异常。

[⬆ 返回目录](#⬆ 返回目录)

🔍 系列模块导航

📝 API 与异步请求规范

一、《Axios 统一封装实战:拦截器配置 + baseURL 优化 + 接口规范,避坑重复代码|API 与异步请求规范篇》
二、《Axios 接口请求规范实战:请求参数 / 响应处理 / 异常兜底,避坑中后台 API 调用混乱|API 与异步请求规范篇》

三、《Axios + Vue 错误处理规范:中后台项目实战,统一捕获系统 / 业务 / 接口异常|API 与异步请求规范篇》
四、《前端实战:Excel 导入导出规范(命名 + 校验 + 错误处理 + 统一交互)|API 与异步请求规范篇》

👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~

📚 系列总览

前端体系化学习完全体:基础 → 规范 → 架构 → 大厂面试

四套系列、百余篇高质量实战文,从入门到进阶,一站式补齐前端核心能力

每个系列完结后,都会整理成一篇完整导航文并附上直达链接,方便大家按顺序、体系化学习。

全套内容持续更新中,敬请期待~

[⬆ 返回目录](#⬆ 返回目录)


技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护。

哪怕每次只吃透一条规范,长期下来,差距会非常明显。

后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。

觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。

我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~

相关推荐
卸任18 小时前
Vibe coding工具使用心得
前端·ai编程
brien919 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance19 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M19 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写19 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李19 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易19 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding20 小时前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马20 小时前
前沿视界
前端
kyriewen20 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员