合理的异常处理能够提升代码的健壮性,从而提升整个系统的可用性。
稍微有点经验的开发人员都会知道用下面的代码来处理代码中的异常。
try
{
// 执行业务逻辑
}
catch ( err )
{
// 处理异常
}
但是,面对成千上万行的代码,这段异常处理应该放在哪里呢?如果只从代码和代码可能产生的异常角度出发,似乎每一行代码都应该加上异常处理,但这显然不现实。下面是我个人在工作中总结的判断。
- 后台Api的调用
- 数据映射
- 事件处理
一个例子。
一前端应用从后台获取数据,然后将其渲染到界面上。
我把这个过程简单的抽象成了3层。在实际的项目中,这个过程可能会很复杂,但基本上能够被这3层覆盖。
api层负责从后台取数据。在这里加上异常主要是为了应对网络和后台服务的不稳定因素。这里异常处理的目标是,能够准确反映错误,以方后台开发人员和平台运维人员排查错误和快速恢复错误。
map层负责将后台拿到的数据转换成前端能够直接展示的数据。在这里加上异常主要是为了应对后台数据结构和前端数据结构不匹配的异常。这里的异常处理的目标是,能够准确反映到底是哪一个函数中的映射出了问题,从而帮助前端开发人员排查错误。
event(事务/事件处理)层负责处理一个完整的业务逻辑。在这里加上异常主要是为了确保程序不会崩溃,清理资源占用,降级处理以及友好展示错误。
通常map层是最复杂的一层,最后再举一个例子详细说明一下。
const renderImages = async (data = {}) => {
try {
const { imageItems, title } = data;
const sortedSrcList = sortBy(imageItems, (item) => convertToInt(item.sortId) || 0);
setTitle(title);
findDom('.show-list').append(sortedSrcList.map(renderImg).join(''));
...
} catch (e) {
showErrMsg(parseError(e));
}
};
上面这段代码中有3个自定义函数convertToInt
, setTitle
, renderImg
,其目的是将拿到的数据渲染到页面上。
如果在这3个函数中不做任何异常处理,当有异常发生时,最外层的try…catch虽然可以截获到异常, 但显示出来的信息反映的是导致错误的原因,并不能准确反映这个异常来自于哪个函数。这种情况如果发生在生产环境下的某个用户的数据上,那时候作为前端开发的“我”,除了后悔当初没有把这个信息弄得更具体一点,还是后悔没有…。
因此,为了避免这种尴尬的情况发生,以renderImg
函数为例,代码可以这样写。
const renderImg = (item) => {
const { picUrl } = item || {};
if (!picUrl) throw 'renderImg: picUrl is not defined';
return `<img src=${picUrl} style='width:100%;height:auto;display:block'/>`;
};
这里的自定义异常添加了函数名称,当这个异常被展示出来时,我们可以知道这个异常来自于哪个函数。如果不在这里添加判断,假设item中的结构发生了变化,导致picUrl为空,这时候图片生成不会报错,但是展现出来的效果就是本该展示的图片没有了。
对于前端开发人员来说,当看到应该显示的图片没有显示时,至少可以从两个方面去排查,一个方面是前端样式定义的问题(兼容性问题);另一个方面是数据问题。我相信这些问题最终是可以被找到的,但是会花多少时间找到呢?5分钟,半小时。如果这个这页面的访问量很大,排查的时间越长,其造成的损失就会越大。这也是为什么要在数据转换的过程中添加自定义异常信息的原因,迅速找到问题,将损失控制在最小的程度内。
可用性是软件系统的众多属性之一,也是最重要的属性之一,这里主要从异常处理的角度来讨论如何确保系统的可用性。这里讨论的是一个思路,但这个思路要不断实践才有效。只要有编程的工作存在,关于异常处理的讨论就不会结束。