Skip to content

前端错误处理最佳实践:从 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

  1. API 层 / 业务逻辑验证失败
js
function checkInput(input) {
    if (!input) throw new Error('输入不能为空');
}
  1. 无法恢复的异常
  • 网络异常、必需数据缺失、类型错误
  • 用 throw 让上层决定如何处理(UI 提示 / 重试 / 上报)

如何配合 try/catch

js
try {
    checkInput(userInput);
    const data = await fetchData();
} catch (err) {
    // 处理用户提示 / 日志上报 / fallback
    console.error(err);
    showErrorMessage(err.message);
}

✅ 原则:

  • 低层 throw,高层 catch
  • 捕获后处理,不要轻易吞掉错误,否则调试困难
  • 只在能处理或上报时捕获

4️⃣ 前端错误层次捕获策略 ​

  1. 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; // 抛给业务层决定处理方式
    }
}
  1. 业务层 / 组件逻辑
js
async function loadUser() {
    try {
        const user = await request('/user');
        renderUser(user);
    } catch (err) {
        showError('加载用户失败'); // 可视化处理
        // 错误不再向上传播,已处理
    }
}
  1. 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>
  1. 全局捕获
js
window.onerror = (msg, url, line, col, error) => { console.log('全局捕获错误', error); }
window.onunhandledrejection = (ev) => { console.log('未捕获 Promise', ev.reason); }

✅ 原则:

  • 全局捕获仅用于日志上报 /兜底,不用于业务处理
  • 不要依赖全局捕获替代局部 try/catch

5️⃣ 总结 & 建议 ​

  1. 错误捕获遵循“从低层 throw 到高层 catch”
  • API 层 throw → 业务/组件层 catch → UI 提示或 fallback
  1. try/catch 捕获了就不会向上传播
  • 如果想继续让上层知道,必须 throw err
  1. 异步函数要用 await 或 .catch
  • 否则 throw 会被 Promise 拒绝捕获
  1. ErrorBoundary 只捕获渲染 / 生命周期错误,不捕获事件和异步错误
  2. 全局捕获仅用于日志和兜底,不替代局部捕获
  3. throw 用于无法处理的异常,让上层处理
  4. 不要吞掉错误,保留信息便于调试

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; // 继续向上抛出
  }
}

当发生错误时,代码的执行顺序是这样的:

  1. 首先,axios 发起请求

  2. 如果发生 HTTP 错误(比如 404):

    • axios 响应拦截器捕获到错误
    • 创建 HttpError 实例并抛出
    • callAPI 函数捕获到错误,显示错误提示,上报错误
    • 最后到达组件的 catch 块
  3. 如果收到业务错误(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 的使用建议 ​

  1. 在适当的位置使用 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();
}
  1. 选择性处理错误
typescript
try {
  await someAPI();
} catch (error) {
  if (error instanceof BusinessError) {
    // 只处理业务错误
    handleBusinessError(error);
  } else {
    // 其他错误继续抛出
    throw error;
  }
}
  1. 避免空捕获
typescript
// 不推荐
try {
  await riskyOperation();
} catch (error) {
  // 什么都不做
}

// 推荐
try {
  await riskyOperation();
} catch (error) {
  // 至少要记录错误
  console.error('操作失败:', error);
  // 或者显示提示
  message.error('操作失败');
}

throw 的使用场景 ​

  1. 参数校验
typescript
function transfer(amount: number) {
  if (amount <= 0) {
    throw new BusinessError(40001, '转账金额必须大于0');
  }
  if (amount > 10000) {
    throw new BusinessError(40002, '单次转账不能超过10000');
  }
  // 继续处理
}
  1. 重新包装错误
typescript
async function uploadFile(file: File) {
  try {
    const result = await uploadAPI.upload(file);
    return result;
  } catch (error) {
    // 转换为业务错误
    throw new BusinessError(
      50001,
      '文件上传失败',
      { originalError: error }
    );
  }
}
  1. 中断执行流程
typescript
function processOrder(order: Order) {
  // 库存检查
  if (!checkStock(order)) {
    throw new BusinessError(40001, '库存不足');
  }
  
  // 支付检查
  if (!checkPayment(order)) {
    throw new BusinessError(40002, '支付失败');
  }
  
  // 继续处理订单
}

深入理解错误传播机制 ​

try-catch 能捕获什么错误? ​

try-catch 可以捕获以下类型的错误:

  1. 显式抛出的错误:通过 throw 语句抛出的错误
typescript
try {
  throw new Error('手动抛出的错误');
} catch (error) {
  console.log('捕获到错误:', error.message);
}
  1. 运行时错误:代码执行过程中的异常
typescript
try {
  const obj = null;
  obj.someMethod(); // 运行时错误:无法调用 null 的方法
} catch (error) {
  console.log('捕获到运行时错误:', error.message);
}
  1. Promise 中的错误:使用 async/await 时
typescript
async function example() {
  try {
    await Promise.reject('Promise 错误');
  } catch (error) {
    console.log('捕获到 Promise 错误:', error);
  }
}
  1. 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 捕获到错误"

错误传播的特点:

  1. 错误会一直向上传播,直到被某个 try-catch 捕获
  2. 如果在 catch 中又使用 throw,错误会继续向上传播
  3. 如果没有被捕获,最终会导致程序崩溃

实际开发中的最佳实践 ​

  1. 分层处理错误
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('系统错误,请稍后重试');
      }
    }
  }
}
  1. 全局错误处理
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 接入与错误监控 ​

  1. 初始化 Sentry
typescript
import * as Sentry from "@sentry/react";

Sentry.init({
  dsn: "your-dsn-here",
  integrations: [
    new Sentry.BrowserTracing(),
  ],
  tracesSampleRate: 1.0,
});
  1. 错误上报服务集成 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
      } : {})
    };
  }
}
  1. 完整的错误监控流程
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;
    }
  }
}
  1. Sentry 的错误分析
  • 错误分组:相似错误会被自动分组
  • 错误追踪:可以看到完整的调用栈
  • 环境信息:浏览器、操作系统等信息
  • 用户影响:受影响的用户数量
  • 错误趋势:错误发生的频率变化

通过这样的错误处理体系,我们可以:

  1. 在开发时及时发现并修复错误
  2. 在生产环境中快速定位问题
  3. 给用户提供友好的错误提示
  4. 收集错误数据进行分析和优化

6. 实践建议与总结 ​

错误处理原则 ​

  1. 分层处理:

    • HTTP 层:处理网络和状态码错误
    • 业务层:处理业务逻辑错误
    • 展示层:处理用户界面反馈
  2. 统一处理:

    • 使用拦截器统一处理通用错误
    • 通过错误类型区分不同错误
    • 提供统一的错误处理接口
  3. 优雅降级:

    • 非致命错误不应影响主要功能
    • 提供友好的错误提示
    • 保留必要的降级方案

代码组织建议 ​

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         // 错误码常量

总结 ​

一个完善的前端错误处理系统应该包含:

  1. 清晰的错误分类和错误码定义
  2. 统一的 HTTP 请求和错误拦截
  3. 规范的业务错误处理流程
  4. 完善的错误监控和上报机制
  5. 友好的用户错误提示

参考资料 ​