文章目录前言鸿蒙应用的错误分类全局错误捕获错误降级策略三层兜底错误码 → 用户可读文案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尽量不让用户看到白屏。exporttypeDataStateT|{status:loading}|{status:success;data:T}|{status:cache;data:T;stale:boolean}|{status:error;error:AppError};exportclassDataFetcherT{privatecacheKey:string;constructor(cacheKey:string){this.cacheKeycacheKey;}asyncfetch(requestFn:()PromiseT,options?:{useCache?:boolean;cacheTTL?:number}):PromiseDataStateT{try{constdataawaitrequestFn();// 请求成功同时更新缓存if(options?.useCache!false){awaitthis.saveCache(data);}return{status:success,data};}catch(error){// 请求失败尝试缓存兜底if(options?.useCache!false){constcachedawaitthis.loadCache();if(cached){Logger.warn(DataFetcher,${this.cacheKey}接口失败使用缓存兜底);return{status:cache,data:cached.data,stale:true};}}// 缓存也没有返回错误状态return{status:error,error:errorasAppError};}}privateasyncsaveCache(data:T):Promisevoid{constcontextgetContext()ascommon.UIAbilityContext;constprefsawaitpreferences.getPreferences(context,data_cache);awaitprefs.put(this.cacheKey,JSON.stringify({data,timestamp:Date.now()}));awaitprefs.flush();}privateasyncloadCache():Promise{data:T;stale:boolean}|null{try{constcontextgetContext()ascommon.UIAbilityContext;constprefsawaitpreferences.getPreferences(context,data_cache);constrawprefs.getSync(this.cacheKey,)asstring;if(!raw)returnnull;constparsedJSON.parse(raw);return{data:parsed.dataasT,stale:true};}catch{returnnull;}}}错误码 → 用户可读文案直接给用户看 “Error code: 40001” 没有任何意义。搞一个映射表把错误码翻译成人话exportclassErrorMessageMapper{privatestaticmessages:Mapnumber,stringnewMap([[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:booleanfalse;StateerrorMessage:string;StateerrorDetail:string;PropfallbackText:string加载失败;onRetry?:()void;BuilderParamcontent:()void;aboutToAppear():void{ErrorHandler.addHandler((error:AppError){// 可以根据 pageName 判断是不是当前区域的错误this.hasErrortrue;this.errorMessageErrorMessageMapper.getMessage(error.code);this.errorDetailerror.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.hasErrorfalse;this.onRetry?.();})}.width(100%).height(100%).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)}else{Column(){this.content()}}}}使用方式很简单把可能出错的区域包起来EntryComponentstruct ProductPage{Stateproducts:Product[][];build(){Column(){// 头部不参与错误隔离HeaderBar({title:商品列表})// 列表区域用 ErrorBoundary 包裹ErrorBoundary({onRetry:()this.loadProducts(),content:(){List(){ForEach(this.products,(item:Product){ListItem(){ProductCard({product:item})}})}}})}}asyncloadProducts(){constfetchernewDataFetcherProduct[](products);constresultawaitfetcher.fetch(()httpService.getProduct[](/products),{useCache:true});switch(result.status){casesuccess:casecache:this.productsresult.data;if(result.statuscache){// 提示用户数据可能不是最新的promptAction.showToast({message:当前显示离线数据});}break;caseerror:ErrorHandler.report(result.error.code,result.error.message,network);break;}}}错误上报最后别忘了把错误发到服务端方便排查问题。上报的时候带上设备信息、页面路径、用户 ID 这些上下文exportclassErrorReporter{privatestaticreportUrl:stringhttps://log.example.com/report;privatestaticqueue:AppError[][];privatestaticbatchSize:number10;staticreport(error:AppError):void{this.queue.push(error);if(this.queue.lengththis.batchSize){this.flush();}}staticasyncflush():Promisevoid{if(this.queue.length0)return;constbatchthis.queue.splice(0);constdeviceInfodevice.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”。错误上报别阻塞主线程。用队列批量上报失败了就静默丢弃。上报请求本身绝对不能触发二次上报不然一个网络抖动就能把日志打爆。
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尽量不让用户看到白屏。exporttypeDataStateT|{status:loading}|{status:success;data:T}|{status:cache;data:T;stale:boolean}|{status:error;error:AppError};exportclassDataFetcherT{privatecacheKey:string;constructor(cacheKey:string){this.cacheKeycacheKey;}asyncfetch(requestFn:()PromiseT,options?:{useCache?:boolean;cacheTTL?:number}):PromiseDataStateT{try{constdataawaitrequestFn();// 请求成功同时更新缓存if(options?.useCache!false){awaitthis.saveCache(data);}return{status:success,data};}catch(error){// 请求失败尝试缓存兜底if(options?.useCache!false){constcachedawaitthis.loadCache();if(cached){Logger.warn(DataFetcher,${this.cacheKey}接口失败使用缓存兜底);return{status:cache,data:cached.data,stale:true};}}// 缓存也没有返回错误状态return{status:error,error:errorasAppError};}}privateasyncsaveCache(data:T):Promisevoid{constcontextgetContext()ascommon.UIAbilityContext;constprefsawaitpreferences.getPreferences(context,data_cache);awaitprefs.put(this.cacheKey,JSON.stringify({data,timestamp:Date.now()}));awaitprefs.flush();}privateasyncloadCache():Promise{data:T;stale:boolean}|null{try{constcontextgetContext()ascommon.UIAbilityContext;constprefsawaitpreferences.getPreferences(context,data_cache);constrawprefs.getSync(this.cacheKey,)asstring;if(!raw)returnnull;constparsedJSON.parse(raw);return{data:parsed.dataasT,stale:true};}catch{returnnull;}}}错误码 → 用户可读文案直接给用户看 “Error code: 40001” 没有任何意义。搞一个映射表把错误码翻译成人话exportclassErrorMessageMapper{privatestaticmessages:Mapnumber,stringnewMap([[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:booleanfalse;StateerrorMessage:string;StateerrorDetail:string;PropfallbackText:string加载失败;onRetry?:()void;BuilderParamcontent:()void;aboutToAppear():void{ErrorHandler.addHandler((error:AppError){// 可以根据 pageName 判断是不是当前区域的错误this.hasErrortrue;this.errorMessageErrorMessageMapper.getMessage(error.code);this.errorDetailerror.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.hasErrorfalse;this.onRetry?.();})}.width(100%).height(100%).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)}else{Column(){this.content()}}}}使用方式很简单把可能出错的区域包起来EntryComponentstruct ProductPage{Stateproducts:Product[][];build(){Column(){// 头部不参与错误隔离HeaderBar({title:商品列表})// 列表区域用 ErrorBoundary 包裹ErrorBoundary({onRetry:()this.loadProducts(),content:(){List(){ForEach(this.products,(item:Product){ListItem(){ProductCard({product:item})}})}}})}}asyncloadProducts(){constfetchernewDataFetcherProduct[](products);constresultawaitfetcher.fetch(()httpService.getProduct[](/products),{useCache:true});switch(result.status){casesuccess:casecache:this.productsresult.data;if(result.statuscache){// 提示用户数据可能不是最新的promptAction.showToast({message:当前显示离线数据});}break;caseerror:ErrorHandler.report(result.error.code,result.error.message,network);break;}}}错误上报最后别忘了把错误发到服务端方便排查问题。上报的时候带上设备信息、页面路径、用户 ID 这些上下文exportclassErrorReporter{privatestaticreportUrl:stringhttps://log.example.com/report;privatestaticqueue:AppError[][];privatestaticbatchSize:number10;staticreport(error:AppError):void{this.queue.push(error);if(this.queue.lengththis.batchSize){this.flush();}}staticasyncflush():Promisevoid{if(this.queue.length0)return;constbatchthis.queue.splice(0);constdeviceInfodevice.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”。错误上报别阻塞主线程。用队列批量上报失败了就静默丢弃。上报请求本身绝对不能触发二次上报不然一个网络抖动就能把日志打爆。