Administrator
发布于 2026-09-03 / 6,678 阅读
0
0

鸿蒙错误体系标准化:错误码设计、异常传播与全局兜底

鸿蒙百篇系列第 72 篇 · 专家

第 71 篇讲了分层架构。分层之后第一个痛点就来了:底层出了错,怎么把错误干净地传到 UI 层,还不丢上下文?这篇专门讲错误体系的工程化设计。

鸿蒙的 ArkTS 有 try-catch,有 BusinessError,有 errorManager 全局监听,有 hiAppEvent 崩溃采集。这些能力散落在系统各处,如果不在架构层做统一设计,线上出了问题你只能看到一堆没有关联的日志,定位不到根因。

一、鸿蒙的错误类型体系

先理清你会在鸿蒙里遇到的几类错误:

错误类型

来源

特征

捕获方式

BusinessError

系统 SDK API

有 code 和 message

try-catch + as BusinessError

Error

ArkTS 运行时

标准 JS 错误对象

try-catch + as Error

NativeCrash

C/C++ 层

SIGSEGV/SIGABRT 信号

hiAppEvent 订阅

JsError

ArkTS 未捕获异常

TypeError/ReferenceError

errorManager 监听

AppFreeze

主线程卡死

事件处理超时

hiAppEvent 订阅

BusinessError vs Error

这是开发者最常困惑的点。规则很简单:

  • 调用鸿蒙系统 API(如 http、rdb、bundleManager)时,catch 到的是 BusinessError,包含 code 和 message

  • 处理纯 ArkTS 业务逻辑时,catch 到的是标准 Error,只有 name 和 message

import { BusinessError } from '@kit.BasicServicesKit';

// 调用系统 API — 捕获 BusinessError
try {
  const prefs = await preferences.getPreferences(context, 'myPrefs');
} catch (err) {
  const businessErr = err as BusinessError;
  console.error(`code: ${businessErr.code}, msg: ${businessErr.message}`);
}

// 纯业务逻辑 — 捕获 Error
try {
  const data = JSON.parse(jsonString);
} catch (err) {
  const error = err as Error;
  console.error(`name: ${error.name}, msg: ${error.message}`);
}

二、业务错误码设计

系统 API 的错误码是数字(如 401 表示参数错误),业务层需要设计自己的错误码体系,但不能和系统码冲突。

建议格式:模块号(2位) + 错误号(3位),模块号从 10 开始,避开系统 1-9 开头的码段。

// common/src/main/ets/errors/AppErrorCode.ets

// 10xx — 网络模块
export const NETWORK_TIMEOUT = 1001;
export const NETWORK_UNAUTHORIZED = 1002;
export const NETWORK_SERVER_ERROR = 1003;

// 11xx — 数据模块
export const DB_QUERY_FAILED = 1101;
export const DB_INSERT_CONFLICT = 1102;
export const PREF_NOT_FOUND = 1103;

// 12xx — 业务模块
export const CART_EMPTY = 1201;
export const PAYMENT_FAILED = 1202;
export const ORDER_NOT_FOUND = 1203;

// 99xx — 未知错误
export const UNKNOWN_ERROR = 9901;

配套的错误描述映射:

const errorMessages: Map<number, string> = new Map([
  [1001, '网络请求超时,请检查网络连接'],
  [1002, '登录已过期,请重新登录'],
  [1003, '服务器开小差了,请稍后重试'],
  [1101, '数据查询失败'],
  [1102, '数据冲突,请刷新后重试'],
  [1201, '购物车是空的'],
  [1202, '支付失败,请重试'],
  [1203, '订单不存在'],
]);

export function getErrorMessage(code: number): string {
  return errorMessages.get(code) ?? '未知错误';
}

三、统一错误类

在公共层定义统一的 AppError 类,封装所有错误信息:

// common/src/main/ets/errors/AppError.ets
import { BusinessError } from '@kit.BasicServicesKit';

export class AppError extends Error {
  code: number;
  module: string;
  timestamp: number;
  cause?: Error;
  traceId?: string;

  constructor(code: number, message: string, options?: {
    module?: string;
    cause?: Error;
    traceId?: string;
  }) {
    super(message);
    this.name = 'AppError';
    this.code = code;
    this.module = options?.module ?? 'unknown';
    this.timestamp = Date.now();
    this.cause = options?.cause;
    this.traceId = options?.traceId;
  }

  // 从 BusinessError 转换
  static fromBusinessError(err: BusinessError, module: string): AppError {
    return new AppError(err.code, err.message, { module, cause: err as Error });
  }

  // 转成可上报的 JSON
  toJSON(): object {
    return {
      code: this.code,
      message: this.message,
      module: this.module,
      timestamp: this.timestamp,
      traceId: this.traceId,
      stack: this.stack?.slice(0, 500),
      cause: this.cause ? {
        name: this.cause.name,
        message: this.cause.message,
      } : null,
    };
  }
}

四、层间错误传播

分层架构(第 71 篇)中,错误从底层向上传播的规则:

数据层 → 能力层 → 特性层 → 产品层
   ↓        ↓        ↓        ↓
 AppError  AppError  AppError  用户提示

每一层做两件事:捕获底层错误,转换成本层的 AppError 继续抛。

能力层:捕获系统错误

// abilities/ability_network/src/main/ets/HttpClient.ets
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { AppError, NETWORK_TIMEOUT, NETWORK_SERVER_ERROR } from '@app/common';

export class HttpClient {
  async get<T>(url: string): Promise<T> {
    try {
      const client = http.createHttp();
      const res = await client.request(url, {
        method: http.RequestMethod.GET,
        connectTimeout: 10000,
        readTimeout: 10000,
      });

      if (res.responseCode >= 500) {
        throw new AppError(NETWORK_SERVER_ERROR, `服务端错误: ${res.responseCode}`, {
          module: 'network',
        });
      }

      return JSON.parse(res.result as string) as T;
    } catch (err) {
      const businessErr = err as BusinessError;

      // 超时
      if (businessErr.code === 2300001) {
        throw new AppError(NETWORK_TIMEOUT, '请求超时', {
          module: 'network',
          cause: businessErr as Error,
        });
      }

      // 已经是 AppError 的直接抛
      if (err instanceof AppError) {
        throw err;
      }

      // 其他未知错误
      throw new AppError(NETWORK_SERVER_ERROR, businessErr.message, {
        module: 'network',
        cause: businessErr as Error,
      });
    }
  }
}

特性层:业务逻辑处理

// features/feature_cart/src/main/ets/CartViewModel.ets
import { AppError, CART_EMPTY } from '@app/common';
import { HttpClient } from '@app/ability_network';

export class CartViewModel {
  constructor(private client: HttpClient) {}

  async checkout(): Promise<void> {
    let cartData;
    try {
      cartData = await this.client.get('/api/cart');
    } catch (err) {
      // 网络错误直接抛,UI 层展示提示
      throw err as AppError;
    }

    if (!cartData.items || cartData.items.length === 0) {
      throw new AppError(CART_EMPTY, '购物车为空', { module: 'cart' });
    }

    // 正常业务流程
  }
}

产品层:UI 兜底

// entry/src/main/ets/pages/CartPage.ets
import { AppError, getErrorMessage } from '@app/common';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct CartPage {
  @State loading: boolean = false;

  async checkout() {
    this.loading = true;
    try {
      await this.viewModel.checkout();
      promptAction.showToast({ message: '下单成功' });
    } catch (err) {
      const appErr = err as AppError;
      const userMsg = getErrorMessage(appErr.code);
      promptAction.showToast({ message: userMsg });
    } finally {
      this.loading = false;
    }
  }
}

五、全局错误监听

即便做了层间传播,总会有漏网之鱼。需要在应用入口注册全局监听。

errorManager 全局监听

// entry/src/main/ets/entryability/EntryAbility.ets
import { errorManager } from '@kit.AbilityKit';
import { hiAppEvent, hilog } from '@kit.PerformanceAnalysisKit';
import process from '@ohos.process';

export default class EntryAbility extends UIAbility {
  onCreate() {
    // 全局错误监听 — 捕获所有未处理的异常
    errorManager.on('globalErrorOccurred', (error: Error) => {
      hilog.error(0x0000, 'AppGlobal', '未捕获异常: %{public}s', error.message);
      hilog.error(0x0000, 'AppGlobal', '堆栈: %{public}s', error.stack ?? '');

      // 上报到服务端
      this.reportError({
        type: 'global_error',
        name: error.name,
        message: error.message,
        stack: error.stack,
      });

      // 同步退出,避免应用处于不稳定状态
      process.exit(0);
    });

    // Promise 未捕获拒绝
    errorManager.on('globalUnhandledRejectionDetected', (reject) => {
      hilog.error(0x0000, 'AppGlobal', '未处理Promise: %{public}s',
        String(reject.reason));
    });
  }

  private async reportError(report: object) {
    // 实际实现:写入文件或上传服务端
    // 下次启动时检查并上传
  }
}

注意:从 API 26.0.0 起,如果用 errorManager 监听了可捕获异常,hiAppEvent 将无法订阅 JsError 崩溃事件。两者选一个。建议用 errorManager 做运行时拦截,用 hiAppEvent 做崩溃后采集。

hiAppEvent 崩溃采集

import { hiAppEvent } from '@kit.PerformanceAnalysisKit';

let watcher: hiAppEvent.Watcher = {
  name: 'CrashWatcher',
  appEventFilters: [
    { domain: 'OS_APP', names: ['JS_CRASH', 'NATIVE_CRASH', 'APP_FREEZE'] },
  ],
  onReceive: (domain: string, appEventGroups: Array<hiAppEvent.AppEventGroup>) => {
    for (const group of appEventGroups) {
      for (const eventInfo of group.appEventInfos) {
        // 崩溃信息包含在 params 中
        const crashType = eventInfo.params['crash_type'];
        const logPath = eventInfo.params['external_log'];
        // 下次启动时上传
      }
    }
  },
};

hiAppEvent.addWatcher(watcher);

六、HiTraceChain 跨层追踪

错误在层间传播时容易丢上下文。用 HiTraceChain 串联一次完整的调用链:

import { hiTraceChain } from '@kit.PerformanceAnalysisKit';

// 能力层开始追踪
async function loadData() {
  const traceId = hiTraceChain.begin('load_product_detail', hiTraceChain.HiTraceFlag.DEFAULT);
  try {
    const data = await this.client.get('/api/products/123');
    hiTraceChain.end(traceId);
    return data;
  } catch (err) {
    // 错误对象携带 traceId
    throw new AppError(1003, '加载失败', {
      module: 'product',
      traceId: traceId.chainId,
      cause: err as Error,
    });
  }
}

日志里就能通过 chainId 把同一请求的多条日志串起来。

七、错误上报策略

不是所有错误都需要上报。按严重程度分级:

级别

示例

处理方式

Fatal

未捕获异常导致闪退

hiAppEvent 自动采集 + 启动后上传

Error

网络请求失败、DB 写入失败

errorManager 拦截 + 延迟上报

Warn

API 调用超时但重试成功

本地日志记录

Info

业务流程异常(如购物车为空)

不上报

上报时的信息最小集:

interface ErrorReport {
  code: number;           // 错误码
  message: string;        // 错误信息
  module: string;         // 来源模块
  timestamp: number;      // 时间戳
  traceId?: string;       // 调用链 ID
  deviceInfo: {           // 设备信息
    brand: string;
    model: string;
    osVersion: string;
    appVersion: string;
  };
  stack?: string;         // 堆栈(截断到 500 字符)
}

八、小结

错误体系的核心不是"怎么 catch",而是"怎么让错误在传播过程中不丢信息"。系统 API 给你 BusinessError,你在能力层包成 AppError,在特性层处理业务逻辑,在产品层展示给用户。每一层做自己的事情,不越界。

下一篇讲性能治理系统——怎么把监控、归因、修复串成闭环。


评论