鸿蒙百篇系列第 72 篇 · 专家
第 71 篇讲了分层架构。分层之后第一个痛点就来了:底层出了错,怎么把错误干净地传到 UI 层,还不丢上下文?这篇专门讲错误体系的工程化设计。
鸿蒙的 ArkTS 有 try-catch,有 BusinessError,有 errorManager 全局监听,有 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 把同一请求的多条日志串起来。
七、错误上报策略
不是所有错误都需要上报。按严重程度分级:
上报时的信息最小集:
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,在特性层处理业务逻辑,在产品层展示给用户。每一层做自己的事情,不越界。
下一篇讲性能治理系统——怎么把监控、归因、修复串成闭环。