前端错误处理最佳实践:从 HTTP 状态码到业务异常
前言
在前端开发中,错误处理是一个经常被忽视但非常重要的话题。一个健壮的前端应用需要优雅地处理各种可能的错误,包括网络错误、HTTP 状态码错误、业务逻辑错误等。本文将详细介绍如何构建一个完整的前端错误处理体系。
错误捕获策略
我们系统梳理一下 前端(JS/TS + React/Vue 等)错误捕获机制,包括 API 层、组件层、全局捕获,以及 try/catch、throw 的使用规范。重点是理解 错误的传播机制 和 捕获范围。
1️⃣ 错误的层次与传播
在前端项目中,错误通常有几个来源和层次:
| 层级 | 错误类型 | 捕获机制 |
|---|---|---|
| API 层 / 数据层 | fetch / axios 请求失败、接口返回错误状态、JSON 解析失败 | Promise.catch 或 async/await try/catch |
| 业务逻辑层 / 工具函数 | JS 函数逻辑错误、类型错误、数组越界等 | try/catch 包裹调用函数 |
| 组件层 / UI 层 | React/Vue 渲染异常、事件处理异常 | React: ErrorBoundary;Vue: errorCaptured / 全局 app.config.errorHandler |
| 全局 | 未捕获的 Promise 拒绝、全局 JS 错误 | window.onerror / window.onunhandledrejection |
核心点:JS 错误默认是向上传播的,从当前函数到调用它的父函数,直到被 try/catch 捕获或最终落到全局。
2️⃣ try/catch 捕获机制
基本原理
js
function a() {
try {
b();
} catch (err) {
console.log('a 捕获到错误', err);
// 如果不 re-throw,错误就不会再传播
}
}
function b() {
throw new Error('测试');
}
a();
// 输出:a 捕获到错误 Error: 测试✅ 要点:
- 捕获了就不会再向上传播,除非你显式 throw。
- 异步函数中的 throw(Promise / async)需要 await 或 catch 才能捕获,否则会成为未捕获的 Promise 拒绝。
异步场景
js
async function fetchData() {
throw new Error('接口异常');
}
// 1. 捕获在调用处
async function run() {
try {
await fetchData();
} catch (err) {
console.log('捕获 fetchData 错误', err);
}
}
run();
// 2. 未捕获
fetchData();
// 未捕获的 Promise Rejection,会触发 window.onunhandledrejection✅ 要点:
- 异步函数错误不会被外层 try/catch 捕获,除非用 await。
- Promise 链需要 .catch() 或 async/await try/catch`
3️⃣ throw 的使用规范
什么时候 throw
- API 层 / 业务逻辑验证失败
js
function checkInput(input) {
if (!input) throw new Error('输入不能为空');
}- 无法恢复的异常
- 网络异常、必需数据缺失、类型错误
- 用 throw 让上层决定如何处理(UI 提示 / 重试 / 上报)
如何配合 try/catch
js
try {
checkInput(userInput);
const data = await fetchData();
} catch (err) {
// 处理用户提示 / 日志上报 / fallback
console.error(err);
showErrorMessage(err.message);
}✅ 原则:
- 低层 throw,高层 catch
- 捕获后处理,不要轻易吞掉错误,否则调试困难
- 只在能处理或上报时捕获
4️⃣ 前端错误层次捕获策略
- API 层
js
async function request(url) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
} catch (err) {
console.error('API 错误', err);
throw err; // 抛给业务层决定处理方式
}
}- 业务层 / 组件逻辑
js
async function loadUser() {
try {
const user = await request('/user');
renderUser(user);
} catch (err) {
showError('加载用户失败'); // 可视化处理
// 错误不再向上传播,已处理
}
}- UI / React 组件层
- React Error Boundary 捕获渲染错误:
js
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
componentDidCatch(error, info) { logError(error, info); }
render() { return this.state.hasError ? <div>出错了</div> : this.props.children; }
}- 事件处理异常需要在事件中 try/catch,否则不会被 ErrorBoundary 捕获:
js
<button onClick={() => {
try { riskyFunc(); } catch(err) { showError(err); }
}}>点击</button>- 全局捕获
js
window.onerror = (msg, url, line, col, error) => { console.log('全局捕获错误', error); }
window.onunhandledrejection = (ev) => { console.log('未捕获 Promise', ev.reason); }✅ 原则:
- 全局捕获仅用于日志上报 /兜底,不用于业务处理
- 不要依赖全局捕获替代局部 try/catch
5️⃣ 总结 & 建议
- 错误捕获遵循“从低层 throw 到高层 catch”
- API 层 throw → 业务/组件层 catch → UI 提示或 fallback
- try/catch 捕获了就不会向上传播
- 如果想继续让上层知道,必须 throw err
- 异步函数要用 await 或 .catch
- 否则 throw 会被 Promise 拒绝捕获
- ErrorBoundary 只捕获渲染 / 生命周期错误,不捕获事件和异步错误
- 全局捕获仅用于日志和兜底,不替代局部捕获
- throw 用于无法处理的异常,让上层处理
- 不要吞掉错误,保留信息便于调试
1. 错误类型分析
HTTP 状态码错误
常见的 HTTP 状态码错误:
javascript
const HTTP_STATUS = {
UNAUTHORIZED: 401, // 未授权
FORBIDDEN: 403, // 禁止访问
NOT_FOUND: 404, // 资源不存在
SERVER_ERROR: 500, // 服务器错误
BAD_GATEWAY: 502, // 网关错误
SERVICE_UNAVAILABLE: 503 // 服务不可用
};业务错误
业务错误通常在 HTTP 200 响应中包含错误信息:
typescript
interface BusinessResponse<T> {
code: number; // 业务状态码
message: string; // 错误信息
data: T | null; // 业务数据
}
// 业务错误码示例
enum BusinessCode {
SUCCESS = 0,
PARAM_ERROR = 40001,
BUSINESS_ERROR = 50001,
SYSTEM_ERROR = 50002
}2. 错误处理架构设计
自定义错误类
typescript
/**
* 自定义业务错误类
*/
class BusinessError extends Error {
constructor(
public code: number,
public message: string,
public data?: any
) {
super(message);
this.name = 'BusinessError';
}
}
/**
* HTTP 错误类
*/
class HttpError extends Error {
constructor(
public status: number,
public message: string
) {
super(message);
this.name = 'HttpError';
}
}Axios 拦截器配置
typescript
/**
* Axios 请求拦截器
*/
axios.interceptors.request.use(
config => {
// 添加 token
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => {
return Promise.reject(error);
}
);
/**
* Axios 响应拦截器
*/
axios.interceptors.response.use(
response => {
const { code, message, data } = response.data;
// 处理业务错误
if (code !== BusinessCode.SUCCESS) {
throw new BusinessError(code, message, data);
}
return data;
},
error => {
if (error.response) {
const { status, data } = error.response;
// 处理特定状态码
switch (status) {
case HTTP_STATUS.UNAUTHORIZED:
// 跳转登录页
router.push('/login');
break;
case HTTP_STATUS.FORBIDDEN:
// 处理无权限
break;
}
throw new HttpError(status, data.message);
}
// 处理网络错误
throw new Error('网络错误,请检查您的网络连接');
}
);3. 错误处理最佳实践
API 调用封装
typescript
/**
* API 请求封装
* @param {Function} apiFunc - API 函数
* @param {Object} options - 配置项
*/
async function callAPI<T>(
apiFunc: () => Promise<T>,
options: {
showError?: boolean;
errorHandler?: (error: any) => void;
} = {}
): Promise<T> {
const { showError = true, errorHandler } = options;
try {
return await apiFunc();
} catch (error) {
// 自定义错误处理
if (errorHandler) {
errorHandler(error);
return Promise.reject(error);
}
// 默认错误处理
if (showError) {
if (error instanceof BusinessError) {
message.error(error.message);
} else if (error instanceof HttpError) {
message.error(`请求失败: ${error.message}`);
} else {
message.error('系统错误,请稍后重试');
}
}
// 错误上报
reportError(error);
return Promise.reject(error);
}
}使用示例
typescript
// API 定义
const userAPI = {
async getUserInfo(userId: string) {
return axios.get(`/api/user/${userId}`);
},
async updateUserInfo(data: any) {
return axios.post('/api/user/update', data);
}
};
// 组件中使用
async function fetchUserData() {
try {
const data = await callAPI(
() => userAPI.getUserInfo('123'),
{
showError: true,
errorHandler: (error) => {
if (error instanceof BusinessError && error.code === BusinessCode.PARAM_ERROR) {
// 特殊处理参数错误
console.log('参数错误');
}
}
}
);
// 处理成功响应
console.log(data);
} catch (error) {
// 这里的错误已经被 callAPI 处理过
console.log('请求失败');
}
}4. 错误处理执行流程
错误处理执行流程详解
让我们通过一个具体的例子来理解错误处理的完整执行流程:
typescript
// 发起请求
async function getUserProfile() {
try {
const data = await callAPI(() => userAPI.getUserInfo('123'));
return data;
} catch (error) {
console.log('在组件中捕获到错误');
throw error; // 继续向上抛出
}
}当发生错误时,代码的执行顺序是这样的:
首先,axios 发起请求
如果发生 HTTP 错误(比如 404):
- axios 响应拦截器捕获到错误
- 创建 HttpError 实例并抛出
- callAPI 函数捕获到错误,显示错误提示,上报错误
- 最后到达组件的 catch 块
如果收到业务错误(200 但 code 非 0):
- axios 响应拦截器检查 code
- 创建 BusinessError 实例并抛出
- 后续流程同上
来看一个更复杂的例子:
typescript
/**
* 用户登录逻辑
*/
async function handleLogin() {
try {
// 1. 表单校验
const values = await form.validateFields();
// 2. 调用登录接口
const data = await callAPI(
() => authAPI.login(values),
{
// 自定义错误处理
errorHandler: (error) => {
if (error instanceof BusinessError) {
if (error.code === 40001) {
message.error('用户名或密码错误');
return; // 在这里处理完毕,不再向上抛出
}
}
throw error; // 其他错误继续抛出
}
}
);
// 3. 登录成功,保存 token
localStorage.setItem('token', data.token);
// 4. 获取用户信息
const userInfo = await callAPI(() => userAPI.getProfile());
// 5. 跳转首页
router.push('/');
} catch (error) {
// 这里只会收到:
// 1. 表单校验错误
// 2. 网络错误
// 3. 未被 errorHandler 处理的业务错误
console.error('登录失败:', error);
}
}关于 try-catch 的使用建议
- 在适当的位置使用 try-catch
typescript
// 不推荐
async function foo() {
try {
await step1();
await step2();
await step3();
} catch (error) {
// 这里捕获了所有错误,无法区分是哪一步出错
console.error(error);
}
}
// 推荐
async function foo() {
try {
await step1();
} catch (error) {
// 处理 step1 的错误
}
try {
await step2();
} catch (error) {
// 处理 step2 的错误
}
// step3 的错误让它继续向上抛出
await step3();
}- 选择性处理错误
typescript
try {
await someAPI();
} catch (error) {
if (error instanceof BusinessError) {
// 只处理业务错误
handleBusinessError(error);
} else {
// 其他错误继续抛出
throw error;
}
}- 避免空捕获
typescript
// 不推荐
try {
await riskyOperation();
} catch (error) {
// 什么都不做
}
// 推荐
try {
await riskyOperation();
} catch (error) {
// 至少要记录错误
console.error('操作失败:', error);
// 或者显示提示
message.error('操作失败');
}throw 的使用场景
- 参数校验
typescript
function transfer(amount: number) {
if (amount <= 0) {
throw new BusinessError(40001, '转账金额必须大于0');
}
if (amount > 10000) {
throw new BusinessError(40002, '单次转账不能超过10000');
}
// 继续处理
}- 重新包装错误
typescript
async function uploadFile(file: File) {
try {
const result = await uploadAPI.upload(file);
return result;
} catch (error) {
// 转换为业务错误
throw new BusinessError(
50001,
'文件上传失败',
{ originalError: error }
);
}
}- 中断执行流程
typescript
function processOrder(order: Order) {
// 库存检查
if (!checkStock(order)) {
throw new BusinessError(40001, '库存不足');
}
// 支付检查
if (!checkPayment(order)) {
throw new BusinessError(40002, '支付失败');
}
// 继续处理订单
}深入理解错误传播机制
try-catch 能捕获什么错误?
try-catch 可以捕获以下类型的错误:
- 显式抛出的错误:通过 throw 语句抛出的错误
typescript
try {
throw new Error('手动抛出的错误');
} catch (error) {
console.log('捕获到错误:', error.message);
}- 运行时错误:代码执行过程中的异常
typescript
try {
const obj = null;
obj.someMethod(); // 运行时错误:无法调用 null 的方法
} catch (error) {
console.log('捕获到运行时错误:', error.message);
}- Promise 中的错误:使用 async/await 时
typescript
async function example() {
try {
await Promise.reject('Promise 错误');
} catch (error) {
console.log('捕获到 Promise 错误:', error);
}
}- API 调用错误:网络请求等异常
typescript
try {
await fetch('不存在的地址');
} catch (error) {
console.log('捕获到网络错误:', error.message);
}错误的传播机制
错误会沿着调用栈向上传播,直到被捕获或到达全局。来看一个例子:
typescript
function level3() {
throw new Error('level 3 错误');
}
function level2() {
try {
level3();
} catch (error) {
console.log('level 2 捕获到错误');
throw error; // 继续向上抛出
}
}
function level1() {
try {
level2();
} catch (error) {
console.log('level 1 捕获到错误');
// 这里不再抛出,错误传播终止
}
}
// 执行结果:
// "level 2 捕获到错误"
// "level 1 捕获到错误"错误传播的特点:
- 错误会一直向上传播,直到被某个 try-catch 捕获
- 如果在 catch 中又使用 throw,错误会继续向上传播
- 如果没有被捕获,最终会导致程序崩溃
实际开发中的最佳实践
- 分层处理错误
typescript
class UserService {
async getUser(id: string) {
try {
const response = await axios.get(`/api/user/${id}`);
return response.data;
} catch (error) {
// 服务层:转换为业务错误,便于上层统一处理
if (error.response?.status === 404) {
throw new BusinessError(40001, '用户不存在');
}
throw error; // 其他错误继续向上抛出
}
}
}
class UserComponent {
async loadUser() {
try {
const user = await userService.getUser('123');
this.setState({ user });
} catch (error) {
// 组件层:处理用户界面相关的错误
if (error instanceof BusinessError) {
message.error(error.message);
} else {
message.error('系统错误,请稍后重试');
}
}
}
}- 全局错误处理
typescript
// 全局错误处理器
window.onerror = (message, source, lineno, colno, error) => {
// 记录错误日志
console.error('Global error:', error);
// 上报到监控系统
errorReport.captureError(error);
// 显示友好的错误提示
message.error('系统遇到问题,请刷新重试');
return true; // 防止错误继续传播
};
// Promise 错误处理
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
event.preventDefault(); // 阻止默认处理
});5. 错误监控与上报
错误上报服务
typescript
/**
* 错误上报服务
*/
class ErrorReportService {
private static instance: ErrorReportService;
private constructor() {
this.initErrorListener();
}
static getInstance() {
if (!ErrorReportService.instance) {
ErrorReportService.instance = new ErrorReportService();
}
return ErrorReportService.instance;
}
/**
* 初始化全局错误监听
*/
private initErrorListener() {
// 监听全局错误
window.onerror = (message, source, lineno, colno, error) => {
this.reportError({
type: 'global',
message,
source,
lineno,
colno,
error
});
};
// 监听 Promise 错误
window.addEventListener('unhandledrejection', (event) => {
this.reportError({
type: 'promise',
message: event.reason
});
});
}
/**
* 上报错误信息
*/
async reportError(error: any) {
try {
const errorInfo = {
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href,
...this.formatError(error)
};
// 发送到错误收集服务
await axios.post('/api/error/report', errorInfo);
} catch (e) {
console.error('Error reporting failed:', e);
}
}
/**
* 格式化错误信息
*/
private formatError(error: any) {
if (error instanceof BusinessError) {
return {
type: 'business',
code: error.code,
message: error.message,
data: error.data
};
}
if (error instanceof HttpError) {
return {
type: 'http',
status: error.status,
message: error.message
};
}
return {
type: 'unknown',
message: error.message || String(error)
};
}
}
// 导出单例
export const errorReport = ErrorReportService.getInstance();Sentry 接入与错误监控
- 初始化 Sentry
typescript
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: "your-dsn-here",
integrations: [
new Sentry.BrowserTracing(),
],
tracesSampleRate: 1.0,
});- 错误上报服务集成 Sentry
typescript
class ErrorReportService {
// ... 其他代码 ...
async reportError(error: any) {
try {
// 1. 格式化错误信息
const errorInfo = this.formatError(error);
// 2. 上报到 Sentry
Sentry.captureException(error, {
extra: errorInfo
});
// 3. 同时上报到自己的服务器
await axios.post('/api/error/report', errorInfo);
} catch (e) {
console.error('Error reporting failed:', e);
}
}
/**
* 格式化错误信息,添加更多上下文
*/
private formatError(error: any) {
return {
// 基础信息
type: error.name,
message: error.message,
stack: error.stack,
// 环境信息
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
// 用户信息
userId: store.getState().user?.id,
// 业务信息
...(error instanceof BusinessError ? {
businessCode: error.code,
businessData: error.data
} : {})
};
}
}- 完整的错误监控流程
typescript
async function example() {
try {
// 1. 业务代码
await someBusinessLogic();
} catch (error) {
// 2. 错误分类处理
if (error instanceof BusinessError) {
// 2.1 业务错误:显示友好提示
message.error(error.message);
} else {
// 2.2 系统错误:显示通用提示
message.error('系统错误,请稍后重试');
}
// 3. 错误上报
await errorReport.reportError(error);
// 4. Sentry 额外信息记录
Sentry.setExtra('currentRoute', router.currentRoute.value);
Sentry.setTag('errorType', error.name);
// 5. 根据需要决定是否继续抛出
if (!(error instanceof BusinessError)) {
throw error;
}
}
}- Sentry 的错误分析
- 错误分组:相似错误会被自动分组
- 错误追踪:可以看到完整的调用栈
- 环境信息:浏览器、操作系统等信息
- 用户影响:受影响的用户数量
- 错误趋势:错误发生的频率变化
通过这样的错误处理体系,我们可以:
- 在开发时及时发现并修复错误
- 在生产环境中快速定位问题
- 给用户提供友好的错误提示
- 收集错误数据进行分析和优化
6. 实践建议与总结
错误处理原则
分层处理:
- HTTP 层:处理网络和状态码错误
- 业务层:处理业务逻辑错误
- 展示层:处理用户界面反馈
统一处理:
- 使用拦截器统一处理通用错误
- 通过错误类型区分不同错误
- 提供统一的错误处理接口
优雅降级:
- 非致命错误不应影响主要功能
- 提供友好的错误提示
- 保留必要的降级方案
代码组织建议
typescript
// 项目结构
src/
├── utils/
│ ├── error/
│ │ ├── types.ts // 错误类型定义
│ │ ├── ErrorClass.ts // 自定义错误类
│ │ └── ErrorHandler.ts // 错误处理器
│ └── http/
│ ├── axios.ts // axios 配置
│ └── interceptors.ts // 拦截器
├── services/
│ ├── api.ts // API 封装
│ └── errorReport.ts // 错误上报服务
└── constants/
└── errorCode.ts // 错误码常量总结
一个完善的前端错误处理系统应该包含:
- 清晰的错误分类和错误码定义
- 统一的 HTTP 请求和错误拦截
- 规范的业务错误处理流程
- 完善的错误监控和上报机制
- 友好的用户错误提示