第6.5篇:错误处理与容错设计——鸿蒙应用健壮性

难度:⭐⭐⭐ 高级
前置知识:第 3.1 ~ 3.9 篇 图像识别、AI 服务与网络请求封装
涉及源文件products/default/src/main/ets/services/AIGenerationService.etsproducts/default/src/main/ets/services/ImageRecognitionService.etsproducts/default/src/main/ets/pages/Index.ets

6.4-1

概述

企业级应用不仅要"能跑",更要"稳得住"。在"画伴梦工厂"中,涉及系统相机调用、HTTP 网络请求、AI 模型推理、图片编解码、文件读写等多种高失败率操作。任何一个环节出错,若未妥善处理,轻则功能异常,重则应用闪退,对儿童用户极不友好。

本文将从"画伴梦工厂"的实际源码出发,系统梳理在 HarmonyOS 应用开发中构建 纵深防御(Defense in Depth) 错误处理体系的最佳实践,涵盖三层保护屏障、异步错误捕获、降级策略、资源清理与 UI 状态守卫。


一、三层防护模型:Try-Catch 边界 + 降级默认值 + 用户提示

项目的错误处理遵循经典的三层防护模型

层级 职责 典型实现
第一层:Try-Catch 边界 捕获异常,防止崩溃扩散 try { ... } catch (error) { ... } finally { ... }
第二层:降级默认值 返回合理的兜底数据 getFallbackResult()pickText()use mock data
第三层:用户友好提示 将错误转化为用户可理解的消息 showNotice()getErrorMessage()

在 Index.ets 的 saveCurrentVideo 方法中,三层防护清晰可见:

private async saveCurrentVideo(): Promise<void> {
  if (this.exportBusy) return;              // UI 守卫(前置防护)
  this.exportBusy = true;
  this.showNotice('正在准备保存视频');        // 操作前提示
  try {
    await VideoExportService.saveToLocal(...);
    this.downloadedActive = true;
    this.showNotice('视频已保存');             // 成功提示
  } catch (error) {
    this.showNotice('保存失败:' + this.getErrorMessage(error as Error));  // 错误提示
  } finally {
    this.exportBusy = false;                 // 资源/状态清理
  }
}

这套模型的核心思想是:每一层都有明确的职责,层与层之间互不替代。Try-Catch 保证不崩溃,降级保证有数据可用,用户提示保证体验不中断。


二、异步错误捕获:async/await 与 try-catch 的黄金组合

在鸿蒙应用中,网络请求、图片处理和 AI 任务全是异步操作。async/await 语法让异步代码看起来像同步代码,但错误处理也必须配套跟上。

2.1 包裹整个异步链路

sendChatPrompt 方法中,整个异步操作被一个 try-catch 块包裹:

private async sendChatPrompt() {
  const prompt = this.chatInput.trim();
  if (prompt === '' || this.chatBusy) return;
  this.chatBusy = true;
  this.chatInput = '';
  // ... 构建消息
  try {
    const generatedImage: GeneratedImage = await AIGenerationService.generateImage(prompt);
    // ... 处理成功结果
  } catch (error) {
    this.showNotice('图片生成失败,请稍后重试');
  } finally {
    this.chatBusy = false;
  }
}

关键要点:

  1. Try 块要足够大:切勿只在 await 行加 try-catch,应将整个请求+处理逻辑包裹在内,这样才能捕获从请求发起、结果解析到状态更新的所有异常。
  2. Catch 块要足够细:给用户一个可操作的提示(“请稍后重试”),而非技术堆栈。
  3. Finally 块必须执行:无论成功还是失败,chatBusy 状态都必须恢复为 false,否则 UI 会永久卡住。

2.2 异步错误传播链

在 AIGenerationService 中,generateVideo 方法调用了 createImg2VideoTaskpollImg2VideoTaskdownloadVideo 等多个异步方法。每个子方法内部都使用 try-catch-finally,但错误会重新抛出(throw),逐层向上传播:

private static async createImg2VideoTask(base64: string): Promise<GeneratedVideoTask> {
  const request = http.createHttp();
  try {
    // ... 发起请求、解析响应
    if (response.responseCode < 200 || response.responseCode >= 300) {
      throw new Error('图片上传接口返回异常:' + response.responseCode.toString());
    }
    // ... 继续解析
  } catch (error) {
    const safeError = error as Error;
    AIGenerationService.logError('createImg2VideoTask failed: ' + AIGenerationService.getErrorMessage(safeError));
    throw safeError;  // 重新抛出,让上层处理
  } finally {
    request.destroy();
  }
}

这种模式的好处是:每层只处理自己关心的清理逻辑(finally),决策逻辑集中在顶层调用者。顶层调用者(如 sendChatPrompt)根据业务场景决定是重试、降级还是提示用户。


三、BusinessError 格式转换与安全错误消息提取

在调用系统 API 或第三方接口时,抛出的错误类型多种多样——可能是 Error 对象、可能带 message 字段、也可能是一个无法直接显示的 JSON 对象。

项目中通过 getErrorMessage 方法对所有错误进行统一的消息提取:

// ImageRecognitionService.ets / AIGenerationService.ets
static getErrorMessage(error: Error): string {
  if (error && error.message && error.message !== '') {
    return error.message;
  }
  return JSON.stringify(error);
}

3.1 为什么要统一格式化?

错误来源 原始格式 getErrorMessage 输出
throw new Error('网络超时') Error: 网络超时 网络超时
JSON parse 失败 SyntaxError: Unexpected token Unexpected token
HTTP 返回 401 Error: 401 Unauthorized 401 Unauthorized
框架抛出系统异常 message 的对象 {"code":201,...}

统一格式化的核心价值在于:无论什么错误结构,前端 UI 都能拿到一个可显示的字符串,不会因为 error.message 为空而显示 undefined

3.2 Index.ets 中的对称实现

Index 页面中的 getErrorMessage 与 Service 层的实现完全一致,确保 UI 层和 Service 层的错误显示风格统一:

private getErrorMessage(error: Error): string {
  if (error && error.message && error.message !== '') return error.message;
  return JSON.stringify(error);
}

四、降级策略:Fallback Result 与兜底数据

当识别或生成服务完全不可用时,应用不能白屏或报错——它应该返回一个合理的默认值,让流程可以继续。

4.1 getFallbackResult:完整的兜底识别结果

ImageRecognitionService 中定义了 DEFAULT_RESULT 常量和 getFallbackResult 静态方法:

const DEFAULT_RESULT: DrawingRecognitionResult = {
  protagonist: '小恐龙',
  scene: '草地和太阳',
  emotion: '快乐探险',
  animationSuggestion: '跳跃、摇尾、看向太阳',
  summary: '识别到儿童手绘中的主角、自然场景和明亮情绪,适合生成轻快的探险动画草稿。',
  elements: [
    { name: '小恐龙', confidence: 96, color: '#D8F7EA' },
    { name: '太阳', confidence: 91, color: '#FFF0DD' },
    { name: '草地', confidence: 88, color: '#EAF8F0' },
    { name: '树木', confidence: 84, color: '#F1EDFF' }
  ]
};

static getFallbackResult(): DrawingRecognitionResult {
  return {
    protagonist: DEFAULT_RESULT.protagonist,
    scene: DEFAULT_RESULT.scene,
    // ... 每个字段都从 DEFAULT_RESULT 获取
  };
}

这个方法的使用场景包括:

  • 图片 URI 为空时直接返回兜底结果,不发起网络请求
  • AI 返回内容缺失(如 protagonist 为空)时,用默认值填充
  • 模型接口异常时,作为候选降级方案

4.2 pickText:单字段的降级守卫

normalizeModelResult 方法使用 pickText 对模型返回的每个字段进行空值检查:

private static pickText(value: string | undefined, fallback: string): string {
  if (value && value.trim() !== '') {
    return value.trim();
  }
  return fallback;
}
return {
  protagonist: ImageRecognitionService.pickText(parsed.protagonist, fallback.protagonist),
  scene: ImageRecognitionService.pickText(parsed.scene, fallback.scene),
  emotion: ImageRecognitionService.pickText(parsed.emotion, fallback.emotion),
  // ...
};

这个简单的函数解决了一个常见痛点:AI 大模型的输出具有不确定性,返回的字段可能是 undefined、空字符串或纯空白。通过 pickText 配合 getFallbackResult 的默认值,实现了字段级别的容错

4.3 clampConfidence:数值范围的守卫

对于置信度等数值字段,clampConfidence 确保数值始终在合法范围内:

private static clampConfidence(value: number | undefined): number {
  if (value === undefined || Number.isNaN(value)) {
    return 86;  // 默认置信度
  }
  return Math.max(0, Math.min(100, Math.round(value)));
}

这种"有界取值"模式确保了即使 AI 返回了异常数据(如 confidence: -5confidence: 250),界面上显示的进度条也不会出界。

4.4 图片压缩失败的降级

在 prepareUploadBase64 方法中,图片压缩失败时直接使用原图:

try {
  const compressedBuffer = await AIGenerationService.compressImageBuffer(sourceBuffer);
  // ... 使用压缩后的 base64
} catch (error) {
  // 压缩失败,降级使用原图
  const sourceBase64 = AIGenerationService.arrayBufferToBase64(sourceBuffer);
  return sourceBase64;
}

五、重试守卫:可重试错误识别

在长时间的 AI 动画生成任务中,网络抖动是常态。pollImg2VideoTask 方法实现了自动重试机制,但并非所有错误都值得重试——认证错误(401)和资源不存在(404)就应该立即失败。

private static canRetryTaskQuery(message: string): boolean {
  return message.indexOf('400') < 0 && message.indexOf('401') < 0 &&
    message.indexOf('403') < 0 && message.indexOf('404') < 0;
}

5.1 重试逻辑的完整闭环

结合 canRetryTaskQuery 的轮询重试模式如下:

while (Date.now() - startedAt < MAX_SEEDANCE_WAIT_MS) {
  let responseBody;
  try {
    responseBody = await AIGenerationService.queryImg2VideoTask(taskId);
  } catch (error) {
    lastErrorMessage = AIGenerationService.getErrorMessage(error as Error);
    if (!AIGenerationService.canRetryTaskQuery(lastErrorMessage)) {
      throw new Error(lastErrorMessage);   // 不可重试,立即抛出
    }
    // 可重试,等待后继续
    await AIGenerationService.sleep(POLL_INTERVAL_MS);
    continue;
  }
  // ... 检查任务状态
}
HTTP 状态码 canRetryTaskQuery 行为
400 Bad Request ❌ 不可重试 请求格式错误,重试无意义
401 Unauthorized ❌ 不可重试 API Key 过期,需重新配置
403 Forbidden ❌ 不可重试 无权限访问,重试无意义
404 Not Found ❌ 不可重试 资源不存在
500+ / 网络超时 ✅ 可重试 服务端临时故障,重试可能成功

5.2 超时兜底

轮询还有一重安全网——超时保护

throw new Error('视频生成超时,请稍后重试' + (lastErrorMessage !== '' ? ',' + lastErrorMessage : ''));

MAX_SEEDANCE_WAIT_MS 被设置为 6 分钟,超过这个时间无论任务是否完成都终止轮询。这防止了用户在极端情况下面临无限等待的困境。


六、资源清理:Finally 块中的确定性释放

HarmonyOS 中的系统资源(HTTP 请求、PixelMap、ImageSource、ImagePacker 等)必须显式释放,否则会造成内存泄漏。

6.1 HTTP 请求的销毁

每个 HTTP 请求方法都使用 try-catch-finally 模式,确保 request.destroy() 一定执行:

private static async createSeedanceTask(...): Promise<string> {
  const request = http.createHttp();
  try {
    // ... 发起请求和处理
  } finally {
    request.destroy();  // 无论成功还是失败,都要销毁请求
  }
}

6.2 图片资源的链式释放

图片压缩涉及多个资源对象,释放顺序有严格要求——必须先释放子对象,再释放父对象:

private static async compressImageBuffer(sourceBuffer: ArrayBuffer): Promise<ArrayBuffer> {
  const source: image.ImageSource = image.createImageSource(sourceBuffer);
  let pixelMap: image.PixelMap | null = null;
  const packer: image.ImagePacker = image.createImagePacker();
  try {
    // ... 解码、缩放、编码
  } finally {
    if (pixelMap !== null) {
      await pixelMap.release();  // 先释放 PixelMap
    }
    await packer.release();       // 再释放 Packer
    await source.release();       // 最后释放 ImageSource
  }
}

6.3 文件句柄的关闭

文件操作也遵循同样的模式——内层 finally 保证文件关闭,外层 finally 保证资源销毁:

const file = fileIo.openSync(path, fileIo.OpenMode.READ_ONLY);
try {
  // ... 文件读取操作
} finally {
  fileIo.closeSync(file);  // 一定关闭文件
}

七、UI 状态守卫:重复操作的预防

在 UI 层面,最典型的错误就是重复点击导致的并发操作。项目中通过布尔状态变量实现了简单的互斥守卫:

// 视频导出守卫
private async saveCurrentVideo(): Promise<void> {
  if (this.exportBusy) return;  // 导出中,忽略重复点击
  this.exportBusy = true;
  // ...
}

// 语音识别守卫
private async startVoiceInput(): Promise<void> {
  if (this.voiceListening) {  // 正在听,执行停止逻辑
    this.voiceListening = false;
    await this.voiceRecognitionService.stop();
    return;
  }
  // ...
}

// AI 图片生成守卫
if (this.chatBusy) return;
this.chatBusy = true;
// ...

7.1 守卫的可视化反馈

更进一步的优化是,在 chatBusy 为 true 时,UI 层也给出视觉反馈:

Button(this.chatBusy ? '生成中' : '发送')
  .backgroundColor(this.chatBusy ? '#A9A0D8' : this.brandPurple)

用户在界面上一眼就能看出当前操作的状态,既防止了重复操作,又消除了用户的不确定性


八、通知系统:用户友好的错误呈现

所有的 catch 块最终都通过 showNotice 方法向用户展示错误信息:

private showNotice(text: string) {
  this.noticeText = text;
}

而在 UI 模板中,NoticeBar 组件会根据 noticeText 非空自动渲染:

@Builder
private NoticeBar() {
  if (this.noticeText !== '') {
    Row() {
      Text(this.noticeText)
        .fontSize(12)
        .fontColor('#FFFFFF')
        .layoutWeight(1)
      Text('x')
        .onClick(() => { this.noticeText = ''; })
    }
    .backgroundColor('#D94C3D')  // 红色警示
    .borderRadius(18)
  }
}

项目中的错误提示遵循几个原则:

原则 说明 示例
以用户为中心 不说技术术语,只说影响和操作 “保存失败”,而非 “TypeError: Cannot read properties”
可操作性 提示用户能做什么 “请稍后重试”
正向引导 即使失败也提供替代路径 “已载入示例画作便于继续流程”
一致性 所有错误走同一个展示通道 统一通过 showNoticeNoticeBar

九、容错策略全景图

将上述所有模式综合起来,可以看到"画伴梦工厂"中的容错体系覆盖了代码执行的每一个关键节点:

用户操作
   │
   ├── UI 状态守卫 ─────── exportBusy / chatBusy / recording
   │
   └── 进入异步操作
        │
        ├── try:
        │     ├── 参数校验(空值检查、格式检查)
        │     ├── 网络请求 / AI 调用 / 本地 IO
        │     └── 结果解析与数据清洗
        │
        ├── catch(error):
        │     ├── getErrorMessage() 格式化
        │     ├── canRetryTaskQuery() 判断是否可重试
        │     │     ├── 可重试 → 休眠后继续
        │     │     └── 不可重试 → 抛出给上层
        │     ├── pickText() / clampConfidence() 字段降级
        │     ├── getFallbackResult() 整体降级
        │     └── showNotice() 通知用户
        │
        └── finally:
              ├── request.destroy()  HTTP 请求销毁
              ├── pixelMap.release()  图片资源释放
              ├── fileIo.closeSync()  文件句柄关闭
              └── chatBusy = false    状态守卫还原

这九个字概括了项目的容错哲学:防得住(guard)、抓得住(try)、兜得住(fallback)、说得清(notice)、收得净(finally)


十、最佳实践总结

以下是本文所有容错模式的速查表,供实际开发中参考:

模式 代码位置 适用场景
UI 状态守卫 if (this.exportBusy) return 防止按钮重复点击、并发操作
同步 try-catch-finally readImageAsBase64 文件读取、JSON 解析等同步操作
异步 try-catch-finally saveCurrentVideo 网络请求、AI 服务等异步操作
统一错误格式化 getErrorMessage() 将各种 Error 类型转为可显示字符串
字段级降级 pickText() / clampConfidence() AI 输出字段缺失或越界
整体降级 getFallbackResult() 服务完全不可用时返回默认 UI 数据
重试守卫 canRetryTaskQuery() HTTP 请求失败的重试决策
超时保护 MAX_SEEDANCE_WAIT_MS 长时间轮询的超时兜底
资源释放 finally { request.destroy() } HTTP、PixelMap、ImageSource、文件句柄
用户通知 showNotice() + NoticeBar 所有错误的对外呈现

总结

本文从"画伴梦工厂"的实际源码出发,系统总结了在鸿蒙应用中构建错误处理与容错设计的方法论:

维度 核心思路 关键代码
三层防护 try-catch 捕获 → 降级兜底 → 用户提示 saveCurrentVideosendChatPrompt
错误格式化 统一提取 error.message getErrorMessage()
降级策略 字段级 + 整体级兜底数据 pickTextgetFallbackResultclampConfidence
重试机制 根据错误类型判断是否可重试 canRetryTaskQuery
资源清理 finally 块中对称释放 request.destroypixelMap.release
UI 守卫 布尔状态互斥锁 exportBusychatBusyrecording
用户通知 统一通道 + 可关闭的 NotificationBar showNotice + NoticeBar

错误处理不是"锦上添花"的功能,而是应用健壮性的基石。一个好的错误处理体系,要做到"悄无声息地处理可恢复错误,不失体面地展示不可恢复错误"——这正是"画伴梦工厂"代码中透露的工程智慧。

在下一篇中,我们将探讨跨页面通信与全局状态管理的最佳实践。


参考源码

本文所有代码均来自项目文件:

  • products/default/src/main/ets/pages/Index.ets — 首页主组件,包含 saveCurrentVideosendChatPromptsaveCurrentVideogetErrorMessageshowNotice 等 UI 守卫和错误处理示例
  • products/default/src/main/ets/services/ImageRecognitionService.ets — 图像识别服务,包含 getFallbackResultpickTextclampConfidencegetErrorMessage 等降级策略
  • products/default/src/main/ets/services/AIGenerationService.ets — AI 生成服务,包含 canRetryTaskQuery、轮询重试、finally 资源清理等高级容错模式
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐