HarmonyOS7 全局异常怎么兜底才靠谱?错误处理和降级架构这样搭
文章目录
- 前言
- 鸿蒙应用的错误分类
- 全局错误捕获
- 错误降级策略:三层兜底
- 错误码 → 用户可读文案
- ErrorBoundary 组件:错误隔离 + 降级 UI
- 错误上报
- 一些心得
前言
你有没有遇到过这种情况:用户在某个页面点了个按钮,接口超时了,页面直接白屏,然后应用闪退。查日志发现是一个未捕获的 Promise rejection。
这类问题在鸿蒙应用里也很常见。错误处理如果只是到处写 try-catch,不仅代码臃肿,还容易漏掉。这篇文章我来搭一套全局错误处理架构,从捕获、分类、降级到上报,一条龙解决。
鸿蒙应用的错误分类
先理清楚错误有哪些类型,不同类型处理方式不一样:
- 同步错误:代码逻辑出错,比如空指针、数组越界。这类最致命,通常会导致页面崩溃。
- 异步错误:Promise rejection 没被 catch,或者 async 函数里抛了异常。
- 网络错误:超时、断网、服务端 5xx。这类最常见,也是最能优化体验的地方。
- 系统错误:权限被拒、存储空间不足、传感器不可用。这类需要引导用户去设置。
全局错误捕获
鸿蒙提供了globalThis上的错误监听能力,我在应用启动时就注册好:
// entry/src/main/ets/entryability/EntryAbility.tsimport{ErrorHandler}from'../common/ErrorHandler';exportdefaultclassEntryAbilityextendsUIAbility{onCreate(want:Want,launchParam:AbilityConstant.LaunchParam):void{// 注册全局错误捕获ErrorHandler.setup();// 初始化日志、网络层等Logger.info('App','应用启动');}}ErrorHandler是核心类,负责注册各类错误监听并分发处理:
import{ErrorReporter}from'./ErrorReporter';import{ErrorMessageMapper}from'./ErrorMessageMapper';exportinterfaceAppError{type:'sync'|'async'|'network'|'system';code:number;message:string;stack?:string;timestamp:number;pageName?:string;}exportclassErrorHandler{privatestatichandlers:Array<(error:AppError)=>void>=[];staticsetup():void{// 捕获未处理的同步错误globalThis.onerror=(msg:string,source:string,lineno:number,colno:number,error:Error)=>{constappError:AppError={type:'sync',code:-1,message:msg,stack:error?.stack,timestamp:Date.now()};ErrorHandler.dispatch(appError);};// 捕获未处理的 Promise rejectionglobalThis.onunhandledrejection=(event:PromiseRejectionEvent)=>{constappError:AppError={type:'async',code:-2,message:String(event.reason),stack:event.reason?.stack,timestamp:Date.now()};ErrorHandler.dispatch(appError);// 阻止默认行为,防止应用崩溃event.preventDefault?.();};Logger.info('ErrorHandler','全局错误捕获已注册');}// 注册错误处理回调staticaddHandler(handler:(error:AppError)=>void):void{this.handlers.push(handler);}// 分发错误到所有处理器privatestaticdispatch(error:AppError):void{// 先上报ErrorReporter.report(error);// 再通知所有注册的处理器for(consthandlerofthis.handlers){try{handler(error);}catch(e){console.error('错误处理器自身出错',e);}}}// 手动上报错误(用于业务层主动上报)staticreport(code:number,message:string,type:AppError['type']='sync'):void{constappError:AppError={type,code,message,timestamp:Date.now()};this.dispatch(appError);}}错误降级策略:三层兜底
核心思路是接口失败 → 缓存兜底 → 兜底 UI,尽量不让用户看到白屏。
exporttypeDataState<T>=|{status:'loading'}|{status:'success';data:T}|{status:'cache';data:T;stale:boolean}|{status:'error';error:AppError};exportclassDataFetcher<T>{privatecacheKey:string;constructor(cacheKey:string){this.cacheKey=cacheKey;}asyncfetch(requestFn:()=>Promise<T>,options?:{useCache?:boolean;cacheTTL?:number}):Promise<DataState<T>>{try{constdata=awaitrequestFn();// 请求成功,同时更新缓存if(options?.useCache!==false){awaitthis.saveCache(data);}return{status:'success',data};}catch(error){// 请求失败,尝试缓存兜底if(options?.useCache!==false){constcached=awaitthis.loadCache();if(cached){Logger.warn('DataFetcher',`${this.cacheKey}接口失败,使用缓存兜底`);return{status:'cache',data:cached.data,stale:true};}}// 缓存也没有,返回错误状态return{status:'error',error:errorasAppError};}}privateasyncsaveCache(data:T):Promise<void>{constcontext=getContext()ascommon.UIAbilityContext;constprefs=awaitpreferences.getPreferences(context,'data_cache');awaitprefs.put(this.cacheKey,JSON.stringify({data,timestamp:Date.now()}));awaitprefs.flush();}privateasyncloadCache():Promise<{data:T;stale:boolean}|null>{try{constcontext=getContext()ascommon.UIAbilityContext;constprefs=awaitpreferences.getPreferences(context,'data_cache');constraw=prefs.getSync(this.cacheKey,'')asstring;if(!raw)returnnull;constparsed=JSON.parse(raw);return{data:parsed.dataasT,stale:true};}catch{returnnull;}}}错误码 → 用户可读文案
直接给用户看 “Error code: 40001” 没有任何意义。搞一个映射表,把错误码翻译成人话:
exportclassErrorMessageMapper{privatestaticmessages:Map<number,string>=newMap([[10001,'网络连接失败,请检查网络设置'],[10002,'请求超时,请稍后重试'],[10003,'服务器开小差了,请稍后再试'],[20001,'登录已过期,请重新登录'],[20002,'账号在其他设备登录'],[20003,'账号已被禁用'],[30001,'内容不存在或已被删除'],[30002,'没有权限执行此操作'],[40001,'存储空间不足,请清理后重试'],[40002,'相机权限未开启,请在设置中允许'],]);staticgetMessage(code:number):string{returnthis.messages.get(code)??'出了点问题,请稍后重试';}// 注册新的错误码映射staticregister(code:number,message:string):void{this.messages.set(code,message);}}ErrorBoundary 组件:错误隔离 + 降级 UI
借鉴 React 的 ErrorBoundary 思路,我实现了一个 ArkUI 版本的错误边界组件。核心是用@State控制显示状态,出错了就展示兜底 UI,不影响其他模块。
@Componentexportstruct ErrorBoundary{@StatehasError:boolean=false;@StateerrorMessage:string='';@StateerrorDetail:string='';@PropfallbackText:string='加载失败';onRetry?:()=>void;@BuilderParamcontent:()=>void;aboutToAppear():void{ErrorHandler.addHandler((error:AppError)=>{// 可以根据 pageName 判断是不是当前区域的错误this.hasError=true;this.errorMessage=ErrorMessageMapper.getMessage(error.code);this.errorDetail=error.message;});}build(){if(this.hasError){Column(){Image($r('app.media.ic_error')).width(80).height(80).margin({bottom:16})Text(this.errorMessage).fontSize(16).fontColor('#333333').margin({bottom:8})Text('点击重试').fontSize(14).fontColor('#007DFF').onClick(()=>{this.hasError=false;this.onRetry?.();})}.width('100%').height('100%').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)}else{Column(){this.content()}}}}使用方式很简单,把可能出错的区域包起来:
@Entry@Componentstruct ProductPage{@Stateproducts:Product[]=[];build(){Column(){// 头部不参与错误隔离HeaderBar({title:'商品列表'})// 列表区域用 ErrorBoundary 包裹ErrorBoundary({onRetry:()=>this.loadProducts(),content:()=>{List(){ForEach(this.products,(item:Product)=>{ListItem(){ProductCard({product:item})}})}}})}}asyncloadProducts(){constfetcher=newDataFetcher<Product[]>('products');constresult=awaitfetcher.fetch(()=>httpService.get<Product[]>('/products'),{useCache:true});switch(result.status){case'success':case'cache':this.products=result.data;if(result.status==='cache'){// 提示用户数据可能不是最新的promptAction.showToast({message:'当前显示离线数据'});}break;case'error':ErrorHandler.report(result.error.code,result.error.message,'network');break;}}}错误上报
最后别忘了把错误发到服务端,方便排查问题。上报的时候带上设备信息、页面路径、用户 ID 这些上下文:
exportclassErrorReporter{privatestaticreportUrl:string='https://log.example.com/report';privatestaticqueue:AppError[]=[];privatestaticbatchSize:number=10;staticreport(error:AppError):void{this.queue.push(error);if(this.queue.length>=this.batchSize){this.flush();}}staticasyncflush():Promise<void>{if(this.queue.length===0)return;constbatch=this.queue.splice(0);constdeviceInfo=device.getCurrent();constreportData={errors:batch,device:deviceInfo.productModel,osVersion:deviceInfo.osFullName,appVersion:'1.0.0',timestamp:Date.now()};try{// 上报请求本身不能再触发错误上报,否则会死循环awaithttp.createHttp().request(this.reportUrl,{method:http.RequestMethod.POST,extraData:reportData,connectTimeout:5000,});}catch{// 上报失败就丢掉,不能影响主流程}}}一些心得
搞完这套错误处理架构之后,我的项目稳定性提升了很多。几个关键点:
降级比报错重要。用户不关心你的接口为什么挂了,他们只想知道还能不能用。缓存兜底 + 提示"离线数据",体验比白屏好太多了。
错误码映射表要维护好。每加一个新接口就检查一下错误码是否有对应文案,别让用户看到 “undefined” 或 “Error: null”。
错误上报别阻塞主线程。用队列批量上报,失败了就静默丢弃。上报请求本身绝对不能触发二次上报,不然一个网络抖动就能把日志打爆。
