HarmonyOS开发中错误处理策略:网络异常统一处理
健壮的网络层,从优雅的错误处理开始
一、背景与动机:为什么需要统一错误处理?
你有没有遇到过这种情况:
用户点击"提交订单",页面卡住了,没有任何提示。用户疑惑地点了第二次、第三次...最后发现是网络断了,但订单已经被提交了三次。
或者这样:
typescript
// 页面A
try {
await api.submitOrder();
} catch (e) {
alert('网络错误');
}
// 页面B
try {
await api.getUser();
} catch (e) {
alert('请求失败');
}
// 页面C
try {
await api.getProduct();
} catch (e) {
alert('出错了');
}
三种不同的错误提示,用户一脸懵:到底是哪里错了?怎么解决?
统一错误处理要解决的就是这个问题:
- 错误分类:网络错误、服务器错误、业务错误,分门别类
- 错误转换:将底层错误转换为用户可理解的提示
- 错误上报:自动记录错误日志,方便排查
- 错误恢复:提供重试、降级等恢复策略
- 用户提示:统一的 Toast、弹窗、错误页面
二、核心原理:错误分类与处理流程
错误处理不是简单的 try-catch,而是一个完整的处理链:
flowchart TB
A[捕获异常] --> B{错误类型判断}
B -->|网络异常| C[NetworkError]
B -->|超时异常| D[TimeoutError]
B -->|HTTP错误| E[HttpError]
B -->|业务错误| F[BusinessError]
B -->|解析错误| G[ParseError]
B -->|未知错误| H[UnknownError]
C --> I[错误转换]
D --> I
E --> I
F --> I
G --> I
H --> I
I --> J[错误日志记录]
J --> K[错误上报]
K --> L{需要用户干预?}
L -->|是| M[显示错误提示]
L -->|否| N[静默处理]
M --> O{可恢复?}
O -->|是| P[提供重试选项]
O -->|否| Q[引导用户操作]
classDef primary fill:#4A90E2,stroke:#2E5C8A,color:#fff
classDef warning fill:#F5A623,stroke:#C17D10,color:#fff
classDef error fill:#E74C3C,stroke:#C0392B,color:#fff
classDef info fill:#7ED321,stroke:#5BA318,color:#fff
class A,I,J,K primary
class C,D,E,F,G,H error
class L,O warning
class M,N,P,Q info
三、代码实战:构建完整错误处理体系
示例1:错误类型定义
typescript
// network/error/types.ets
/**
* 错误码枚举
* 定义所有可能的错误码,方便统一管理
*/
export enum ErrorCode {
// 网络相关错误 (-1 ~ -99)
NETWORK_ERROR = -1, // 网络不可用
NETWORK_TIMEOUT = -2, // 请求超时
NETWORK_DISCONNECT = -3, // 网络断开
NETWORK_SLOW = -4, // 网络慢
// HTTP协议错误 (100 ~ 599)
HTTP_BAD_REQUEST = 400, // 请求参数错误
HTTP_UNAUTHORIZED = 401, // 未授权
HTTP_FORBIDDEN = 403, // 禁止访问
HTTP_NOT_FOUND = 404, // 资源不存在
HTTP_METHOD_NOT_ALLOWED = 405, // 方法不允许
HTTP_REQUEST_TIMEOUT = 408, // 请求超时
HTTP_CONFLICT = 409, // 冲突
HTTP_TOO_MANY_REQUESTS = 429, // 请求过多
HTTP_INTERNAL_ERROR = 500, // 服务器内部错误
HTTP_BAD_GATEWAY = 502, // 网关错误
HTTP_SERVICE_UNAVAILABLE = 503, // 服务不可用
HTTP_GATEWAY_TIMEOUT = 504, // 网关超时
// 业务错误 (1000 ~ 1999)
BIZ_SUCCESS = 0, // 成功
BIZ_PARAM_ERROR = 1001, // 参数错误
BIZ_DATA_NOT_FOUND = 1002, // 数据不存在
BIZ_PERMISSION_DENIED = 1003, // 权限不足
BIZ_TOKEN_EXPIRED = 1004, // Token过期
BIZ_TOKEN_INVALID = 1005, // Token无效
BIZ_USER_BANNED = 1006, // 用户被封禁
BIZ_OPERATION_FAILED = 1007, // 操作失败
// 客户端错误 (2000 ~ 2999)
CLIENT_PARSE_ERROR = 2001, // 数据解析错误
CLIENT_CACHE_ERROR = 2002, // 缓存错误
CLIENT_STORAGE_ERROR = 2003, // 存储错误
// 未知错误
UNKNOWN = -9999
}
/**
* 错误严重程度
*/
export enum ErrorSeverity {
LOW = 'low', // 低:不影响使用,静默处理
MEDIUM = 'medium', // 中:影响当前操作,提示用户
HIGH = 'high', // 高:影响整体使用,弹窗提示
CRITICAL = 'critical' // 严重:应用无法使用,阻断操作
}
/**
* 错误分类
*/
export enum ErrorCategory {
NETWORK = 'network', // 网络错误
HTTP = 'http', // HTTP错误
BUSINESS = 'business', // 业务错误
CLIENT = 'client', // 客户端错误
UNKNOWN = 'unknown' // 未知错误
}
/**
* API错误基类
* 所有网络错误的基类,包含丰富的错误信息
*/
export class ApiError extends Error {
/** 错误码 */
readonly code: ErrorCode;
/** HTTP状态码 */
readonly status: number;
/** 错误分类 */
readonly category: ErrorCategory;
/** 严重程度 */
readonly severity: ErrorSeverity;
/** 原始错误数据 */
readonly rawData?: any;
/** 是否可重试 */
readonly retryable: boolean;
/** 用户友好的错误提示 */
readonly userMessage: string;
/** 错误发生时间 */
readonly timestamp: number;
constructor(config: {
message: string;
code: ErrorCode;
status?: number;
category?: ErrorCategory;
severity?: ErrorSeverity;
rawData?: any;
retryable?: boolean;
userMessage?: string;
}) {
super(config.message);
this.name = 'ApiError';
this.code = config.code;
this.status = config.status ?? 0;
this.category = config.category ?? ErrorCategory.UNKNOWN;
this.severity = config.severity ?? ErrorSeverity.MEDIUM;
this.rawData = config.rawData;
this.retryable = config.retryable ?? false;
this.userMessage = config.userMessage ?? config.message;
this.timestamp = Date.now();
}
/**
* 转换为JSON(用于日志记录)
*/
toJSON(): Record<string, any> {
return {
name: this.name,
message: this.message,
code: this.code,
status: this.status,
category: this.category,
severity: this.severity,
retryable: this.retryable,
userMessage: this.userMessage,
timestamp: this.timestamp,
stack: this.stack
};
}
}
/**
* 网络错误
*/
export class NetworkError extends ApiError {
constructor(message: string = '网络连接失败', code: ErrorCode = ErrorCode.NETWORK_ERROR) {
super({
message,
code,
category: ErrorCategory.NETWORK,
severity: ErrorSeverity.HIGH,
retryable: true,
userMessage: '网络不给力,请检查网络设置'
});
this.name = 'NetworkError';
}
}
/**
* 超时错误
*/
export class TimeoutError extends ApiError {
constructor(message: string = '请求超时') {
super({
message,
code: ErrorCode.NETWORK_TIMEOUT,
category: ErrorCategory.NETWORK,
severity: ErrorSeverity.MEDIUM,
retryable: true,
userMessage: '请求超时,请稍后重试'
});
this.name = 'TimeoutError';
}
}
/**
* HTTP错误
*/
export class HttpError extends ApiError {
constructor(status: number, message: string, data?: any) {
const severity = status >= 500 ? ErrorSeverity.HIGH : ErrorSeverity.MEDIUM;
const retryable = [408, 429, 500, 502, 503, 504].includes(status);
super({
message,
code: status as ErrorCode,
status,
category: ErrorCategory.HTTP,
severity,
rawData: data,
retryable,
userMessage: HttpError.getUserMessage(status)
});
this.name = 'HttpError';
}
/**
* 根据状态码获取用户提示
* @private
*/
private static getUserMessage(status: number): string {
const messages: Record<number, string> = {
400: '请求参数错误',
401: '请先登录',
403: '没有访问权限',
404: '请求的资源不存在',
408: '请求超时',
429: '请求过于频繁,请稍后再试',
500: '服务器开小差了,请稍后再试',
502: '网关错误',
503: '服务暂时不可用',
504: '网关超时'
};
return messages[status] || `请求失败(${status})`;
}
}
/**
* 业务错误
*/
export class BusinessError extends ApiError {
constructor(code: number, message: string, data?: any) {
super({
message,
code: code as ErrorCode,
category: ErrorCategory.BUSINESS,
severity: ErrorSeverity.MEDIUM,
rawData: data,
retryable: false,
userMessage: message
});
this.name = 'BusinessError';
}
}
示例2:错误处理器
typescript
// network/error/ErrorHandler.ets
import {
ApiError, NetworkError, TimeoutError, HttpError, BusinessError,
ErrorCode, ErrorCategory, ErrorSeverity
} from './types';
import { promptAction } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
/**
* 错误处理器配置
*/
export interface ErrorHandlerConfig {
/** 是否显示错误提示 */
showToast?: boolean;
/** 是否记录日志 */
logError?: boolean;
/** 是否上报错误 */
reportError?: boolean;
/** 自定义错误提示映射 */
customMessages?: Map<ErrorCode, string>;
/** 错误回调 */
onError?: (error: ApiError) => void;
}
/**
* 错误处理器
* 统一处理所有网络错误
*/
export class ErrorHandler {
private config: ErrorHandlerConfig;
constructor(config: ErrorHandlerConfig = {}) {
this.config = {
showToast: true,
logError: true,
reportError: true,
...config
};
}
/**
* 处理错误
* @param error 原始错误
* @returns 转换后的ApiError
*/
handle(error: any): ApiError {
// 1. 转换为ApiError
const apiError = this.transform(error);
// 2. 记录日志
if (this.config.logError) {
this.logError(apiError);
}
// 3. 上报错误
if (this.config.reportError) {
this.reportError(apiError);
}
// 4. 显示用户提示
if (this.config.showToast && apiError.severity !== ErrorSeverity.LOW) {
this.showUserMessage(apiError);
}
// 5. 执行自定义回调
if (this.config.onError) {
this.config.onError(apiError);
}
return apiError;
}
/**
* 转换错误为ApiError
* @private
*/
private transform(error: any): ApiError {
// 已经是ApiError,直接返回
if (error instanceof ApiError) {
return error;
}
// 处理原生网络错误
if (this.isNetworkError(error)) {
return new NetworkError();
}
// 处理超时错误
if (this.isTimeoutError(error)) {
return new TimeoutError();
}
// 处理HTTP错误响应
if (this.isHttpError(error)) {
return new HttpError(
error.responseCode || error.status,
error.message || 'HTTP Error',
error.result || error.data
);
}
// 处理业务错误
if (this.isBusinessError(error)) {
return new BusinessError(
error.code,
error.message,
error.data
);
}
// 未知错误
return new ApiError({
message: error.message || '未知错误',
code: ErrorCode.UNKNOWN,
category: ErrorCategory.UNKNOWN,
severity: ErrorSeverity.MEDIUM,
rawData: error
});
}
/**
* 判断是否为网络错误
* @private
*/
private isNetworkError(error: any): boolean {
// HarmonyOS网络错误码
const networkErrorCodes = [2300001, 2300002, 2300003];
return networkErrorCodes.includes(error.code) ||
error.message?.includes('Network') ||
error.message?.includes('网络');
}
/**
* 判断是否为超时错误
* @private
*/
private isTimeoutError(error: any): boolean {
return error.code === 2300002 ||
error.message?.includes('Timeout') ||
error.message?.includes('超时');
}
/**
* 判断是否为HTTP错误
* @private
*/
private isHttpError(error: any): boolean {
return error.responseCode >= 400 || error.status >= 400;
}
/**
* 判断是否为业务错误
* @private
*/
private isBusinessError(error: any): boolean {
return error.code !== undefined &&
error.code !== 0 &&
error.message !== undefined;
}
/**
* 记录错误日志
* @private
*/
private logError(error: ApiError): void {
const logContent = `
┌────────────────────────────────────────
│ [错误日志] ${new Date(error.timestamp).toISOString()}
├────────────────────────────────────────
│ 类型: ${error.name}
│ 分类: ${error.category}
│ 严重程度: ${error.severity}
│ 错误码: ${error.code}
│ HTTP状态: ${error.status}
│ 消息: ${error.message}
│ 用户提示: ${error.userMessage}
│ 可重试: ${error.retryable}
│ 堆栈: ${error.stack?.split('\n').slice(0, 3).join('\n')}
└────────────────────────────────────────
`;
// 根据严重程度选择日志级别
if (error.severity === ErrorSeverity.CRITICAL) {
hilog.error(0x0000, 'NetworkError', logContent);
} else if (error.severity === ErrorSeverity.HIGH) {
hilog.warn(0x0000, 'NetworkError', logContent);
} else {
hilog.info(0x0000, 'NetworkError', logContent);
}
}
/**
* 上报错误到服务器
* @private
*/
private reportError(error: ApiError): void {
// 这里可以集成Sentry、Bugly等错误监控平台
// 简化示例:只记录到本地
console.info('[ErrorHandler] 错误已上报:', error.code);
// 实际项目中可以:
// 1. 收集设备信息、用户信息
// 2. 打包错误数据
// 3. 发送到错误监控平台
}
/**
* 显示用户提示
* @private
*/
private showUserMessage(error: ApiError): void {
// 使用自定义消息或默认消息
let message = this.config.customMessages?.get(error.code) || error.userMessage;
// 根据严重程度选择提示方式
if (error.severity === ErrorSeverity.CRITICAL) {
// 严重错误:弹窗提示
promptAction.showDialog({
title: '错误',
message: message,
buttons: [{ text: '确定', color: '#E74C3C' }]
});
} else {
// 普通错误:Toast提示
promptAction.showToast({
message: message,
duration: 3000
});
}
}
}
示例3:错误处理拦截器
typescript
// network/error/ErrorInterceptor.ets
import { Interceptor, Response, RequestConfig } from '../types';
import { ErrorHandler, ApiError } from './ErrorHandler';
/**
* 错误处理拦截器
* 拦截所有请求错误,统一处理
*/
export class ErrorInterceptor implements Interceptor {
private errorHandler: ErrorHandler;
constructor(errorHandler: ErrorHandler) {
this.errorHandler = errorHandler;
}
/**
* 请求拦截:无操作
*/
async beforeRequest(config: RequestConfig): Promise<RequestConfig> {
return config;
}
/**
* 响应拦截:检查错误
*/
async afterResponse<T>(response: Response<T>): Promise<Response<T>> {
// 检查HTTP状态码
if (response.status >= 200 && response.status < 300) {
// 请求成功,检查业务状态码
const data = response.data as any;
if (data && typeof data === 'object' && 'code' in data) {
// 后端统一返回格式:{ code, message, data }
if (data.code !== 0) {
// 业务错误
throw this.errorHandler.handle({
code: data.code,
message: data.message || data.msg || '业务错误',
data: data.data
});
}
// 业务成功,返回真实数据
return {
...response,
data: data.data as T
};
}
// 直接返回数据
return response;
}
// HTTP错误,抛出
throw this.errorHandler.handle({
responseCode: response.status,
message: 'HTTP Error',
result: response.data
});
}
}
示例4:错误边界组件
typescript
// components/ErrorBoundary.ets
import { ApiError, ErrorCategory } from '../network/error/types';
import { promptAction } from '@kit.ArkUI';
/**
* 错误边界组件
* 用于包裹可能出错的UI组件,提供降级显示
*/
@Component
export struct ErrorBoundary {
@Prop hasError: boolean = false;
@Prop error: ApiError | null = null;
@BuilderParam defaultContent: () => void;
@BuilderParam errorContent?: () => void;
build() {
if (this.hasError && this.error) {
// 显示错误UI
if (this.errorContent) {
this.errorContent();
} else {
this.defaultErrorUI();
}
} else {
// 显示正常内容
this.defaultContent();
}
}
/**
* 默认错误UI
*/
@Builder
defaultErrorUI() {
Column() {
// 错误图标
Image($r('app.media.ic_error'))
.width(80)
.height(80)
.margin({ bottom: 16 })
// 错误提示
Text(this.error?.userMessage || '出错了')
.fontSize(16)
.fontColor('#666666')
.margin({ bottom: 24 })
// 重试按钮(如果可重试)
if (this.error?.retryable) {
Button('重试')
.type(ButtonType.Capsule)
.backgroundColor('#4A90E2')
.fontColor(Color.White)
.onClick(() => {
// 通知父组件重试
this.hasError = false;
})
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#F5F5F5')
}
/**
* 捕获错误
*/
catch(error: ApiError) {
this.error = error;
this.hasError = true;
}
/**
* 重置错误状态
*/
reset() {
this.error = null;
this.hasError = false;
}
}
示例5:页面中的错误处理实战
typescript
// pages/UserPage.ets
import { UserAPI } from '../api/UserAPI';
import { ApiError, NetworkError } from '../network/error/types';
import { ErrorHandler } from '../network/error/ErrorHandler';
@Entry
@Component
struct UserPage {
@State user: User | null = null;
@State loading: boolean = false;
@State error: ApiError | null = null;
private userApi: UserAPI = new UserAPI();
private errorHandler: ErrorHandler = new ErrorHandler({
showToast: false, // 页面自己控制提示
onError: (err) => {
this.error = err;
}
});
aboutToAppear() {
this.loadUser();
}
/**
* 加载用户信息
*/
async loadUser() {
this.loading = true;
this.error = null;
try {
const response = await this.userApi.getCurrentUser();
this.user = response.data;
} catch (e) {
// 统一错误处理
const apiError = this.errorHandler.handle(e);
this.error = apiError;
} finally {
this.loading = false;
}
}
build() {
Column() {
if (this.loading) {
// 加载中
this.loadingUI();
} else if (this.error) {
// 错误状态
this.errorUI();
} else if (this.user) {
// 正常显示
this.contentUI();
}
}
.width('100%')
.height('100%')
}
/**
* 加载UI
*/
@Builder
loadingUI() {
Column() {
LoadingProgress()
.width(48)
.height(48)
.color('#4A90E2')
Text('加载中...')
.fontSize(14)
.fontColor('#999999')
.margin({ top: 16 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
/**
* 错误UI
*/
@Builder
errorUI() {
Column() {
// 根据错误类型显示不同图标
if (this.error?.category === 'network') {
Image($r('app.media.ic_network_error'))
.width(100)
.height(100)
} else {
Image($r('app.media.ic_error'))
.width(100)
.height(100)
}
Text(this.error?.userMessage || '出错了')
.fontSize(16)
.fontColor('#666666')
.margin({ top: 16, bottom: 24 })
// 重试按钮
if (this.error?.retryable) {
Button('重新加载')
.type(ButtonType.Capsule)
.backgroundColor('#4A90E2')
.fontColor(Color.White)
.width(120)
.onClick(() => {
this.loadUser();
})
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#F5F5F5')
}
/**
* 内容UI
*/
@Builder
contentUI() {
Column() {
// 头像
Image(this.user?.avatar)
.width(80)
.height(80)
.borderRadius(40)
.margin({ bottom: 16 })
// 用户名
Text(this.user?.name || '')
.fontSize(20)
.fontWeight(FontWeight.Bold)
// 邮箱
Text(this.user?.email || '')
.fontSize(14)
.fontColor('#999999')
.margin({ top: 8 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
四、踩坑与注意事项
坑1:错误信息泄露
问题:直接把后端错误消息显示给用户,可能泄露敏感信息。
解决:使用用户友好的提示:
typescript
// ❌ 错误:直接显示后端消息
alert(error.message); // "SQL syntax error near 'select'"
// ✅ 正确:使用用户友好提示
alert(error.userMessage); // "服务器开小差了,请稍后再试"
坑2:错误吞没
问题:catch 块中没有处理错误,导致错误被"吞没"。
typescript
// ❌ 错误:空catch
try {
await api.getData();
} catch (e) {
// 什么都不做,错误被吞没
}
// ✅ 正确:至少记录日志
try {
await api.getData();
} catch (e) {
console.error('请求失败:', e);
throw e; // 继续抛出,让上层处理
}
坑3:异步错误未捕获
问题:Promise 的错误没有被 try-catch 捕获。
typescript
// ❌ 错误:async函数外的错误不会被捕获
try {
api.getData().then(data => {
// 这里抛错不会被外层catch捕获
throw new Error('处理失败');
});
} catch (e) {
// 捕获不到
}
// ✅ 正确:使用async/await
try {
const data = await api.getData();
// 处理数据
} catch (e) {
// 可以捕获
}
坑4:错误循环上报
问题:错误上报接口失败,又触发错误处理,导致循环。
解决:错误上报接口跳过错误处理:
typescript
// 错误上报使用独立的http实例,不走拦截器
const reportHttp = http.createHttp();
// 不添加ErrorInterceptor
五、HarmonyOS 6 适配要点
1. hilog API 增强
typescript
// HarmonyOS 6 支持更丰富的日志功能
import { hilog } from '@kit.PerformanceAnalysisKit';
// 支持格式化参数
hilog.error(0x0000, 'MyTag', 'Error occurred: %{public}s, code: %{public}d',
error.message, error.code);
// 支持不同日志级别
hilog.debug(0x0000, 'MyTag', 'Debug message');
hilog.info(0x0000, 'MyTag', 'Info message');
hilog.warn(0x0000, 'MyTag', 'Warning message');
hilog.error(0x0000, 'MyTag', 'Error message');
hilog.fatal(0x0000, 'MyTag', 'Fatal message');
2. promptAction 增强
typescript
// HarmonyOS 6 Toast支持更多配置
promptAction.showToast({
message: '操作成功',
duration: 2000,
bottom: 100, // 距离底部距离
showMode: promptAction.ToastShowMode.TOP_MOVED
});
// Dialog支持更多按钮样式
promptAction.showDialog({
title: '确认删除',
message: '删除后无法恢复',
buttons: [
{ text: '取消', color: '#666666' },
{ text: '删除', color: '#E74C3C' }
]
});
3. 错误码标准化
typescript
// HarmonyOS 6 定义了更详细的错误码
// 网络错误码范围:2300001 - 2300999
// 2300001: 网络不可用
// 2300002: 连接超时
// 2300003: 协议错误
// 2300004: URL格式错误
// 2300005: DNS解析失败
// ...
// 可以根据系统错误码映射
if (error.code >= 2300001 && error.code <= 2300999) {
// 网络相关错误
}
六、总结一下下
统一错误处理是网络层的"安全网",让应用在异常情况下也能优雅应对:
| 错误类型 | 分类 | 严重程度 | 可重试 | 用户提示 |
|---|---|---|---|---|
| NetworkError | network | HIGH | ✅ | 网络不给力 |
| TimeoutError | network | MEDIUM | ✅ | 请求超时 |
| HttpError(401) | http | HIGH | ❌ | 请先登录 |
| HttpError(404) | http | MEDIUM | ❌ | 资源不存在 |
| HttpError(500) | http | HIGH | ✅ | 服务器开小差了 |
| BusinessError | business | MEDIUM | ❌ | 后端消息 |
记住几个原则:
- ✅ 错误要分类,不同类型不同处理
- ✅ 提示要友好,别让用户看技术错误
- ✅ 日志要完整,方便排查问题
- ✅ 上报要及时,监控线上质量
- ✅ 降级要优雅,别让应用崩溃
下一篇我们深入请求取消与并发,看看 AbortController 的妙用。
💡 最佳实践提示:建议在项目中建立错误码文档,统一管理所有错误码和对应的处理策略,方便团队协作。