抛出错误以及返回错误

最近在思考错误的机制,通常我们有两种使用错误的方式,一种就是返回错误,另一种则是抛出错误

// 返回错误
const [error, result] = await service.getList(data);
// 抛出错误
const initData = async () => {
const res = await service.getList(data);
if (!res) throw 'service error';
};

单单从错误使用来说其实没有什么意义,但是我们不妨思考一下,我们的业务代码其实也可以通过两种模型来组织,所以下面将一系列抽象的不符合预期的行为统一将其称之为错误,这样来思考我们的代码形式

返回错误

返回错误其实是比较常用的一种类型,比如 node 中经常就这么使用,go 语言也是直接返回错误的,我们要先判断是否存在错误然后进行再下一步

fs.readdir(configPath, (error, files) => {
if (err) console.log(err);
});

返回错误的最大特点就是,不用担心程序的异常,所有的错误都要层层拦截处理,并且异常的类型可推断,完全不用担心内部黑箱

但是最大的问题则是,代码过于繁琐,无法中断流程,必须层层传递,举个例子;

const FormModal = ({ onSubmit, children }) => {
const onFinish = async values => {
modal.load(); // 弹窗开始加载显示loading
const [error, res] = await onSubmit(values); // 提交数据
modal.fail(); // 弹窗隐藏loading
if (error) return;
modal.cancel(); // 弹窗关闭
};
return (
<Modal>
<Form onFinish={onFinish}>{children}</Form>{' '}
</Modal>
);
};
const QuickEdit = ({ submit }) => {
const onSubmit = async values => {
const [error, res] = await submit(values); // 提交数据
if (!error) others(); // 其他操作
return [error, res]; // 返回正确
};
return (
<FormModal onSubmit={onSubmit}>
<Form.Item />
</FormModal>
);
};
const Page = () => {
const submit = async values => {
const [error, res] = await service.edit(values, ids); // 提交数据
if (!error) initData(); // 刷新列表
return [error, res];
};
return <QuickEdit submit={submit} />;
};

上面是个三层嵌套的组件,每个组件内部都带有自己的操作逻辑,看上去就是一堆的判断,正儿八经的代码没写几行,全是各种流程,而且你必须层层传递,一旦中间有一层没有处理,整个流程就会断,在实际的开发当中比较常见,而且我们还没加上各种 try catch ,一旦加上去会导致冗余的代码越来越长

抛出错误

抛出错误这个其实 java 用的比较多,直接抛出一个 Exception 即可,但是 js 中只有 Error 类,而这些也一般都是用于代码执行错误等,这种错误虽然可以尽力避免,但是不能完全保证消除, 这也导致了 js 中抛出异常有点麻烦

但是如果我们考虑这种方法,那么我们不妨来尝试改造下上面的例子

const FormModal = ({onSubmit,children}) => {
const onFinish = async (values) => {
modal.load();
try{
await onSubmit(values)
modal.cancel(),
}catch{
modal.fail()
}
}
return <Modal><Form onFinish={onFinish}>{children}</Form> </Modal>
}
const QuickEdit = ({submit})=>{
const onSubmit = async (values)=>{
await submit(values); // 提交数据
others() // 其他操作
}
return <FormModal onSubmit={onSubmit}><Form.Item /></FormModal>
}
const Page = ()=>{
const submit = async (values)=>{
await service.edit(values, ids); // 提交数据
initData(); // 刷新列表
}
return <QuickEdit submit={submit}/>
}

可以看出,从上面的代码逻辑来看,我们默认所有的执行都是成功的,而一旦有不符合预期的行为,那么就会直接抛出错误来中断流程,我们只需要在最后的函数中拦截错误即可, 具体流程如下:

FormModal.onSubmit = ()=>{
try{
QuickEdit.onSubmit(()=>{
Page.submit(()=>{
await service.edit(values, ids);
initData();
})
others()
})
modal.cancel(),
}catch{
modal.fail()
}
}

这样看来,我们完全只需要聚焦于业务代码,甚至都不用层层传递错误,代码也是十分的简洁清晰,可以说是优势很大

但是,它的缺点在于,一旦我们需要对错误进行层层处理,那么它就会退化成比返回错误更复杂的情况

const FormModal = ({onSubmit,children}) => {
const onFinish = async (values) => {
modal.load();
try{
await onSubmit(values)
modal.cancel(),
}catch{
modal.fail()
}
}
return <Modal><Form onFinish={onFinish}>{children}</Form> </Modal>
}
const QuickEdit = ({submit})=>{
const onSubmit = async (values)=>{
await submit(values).catch(err=>{handlerError(err);throw 'err'})
others() // 其他操作
}
return <FormModal onSubmit={onSubmit}><Form.Item /></FormModal>
}
const Page = ()=>{
const submit = async (values)=>{
await service.edit(values, ids).catch(err=>{handlerError(err);throw 'err'})
initData(); // 刷新列表
}
return <QuickEdit submit={submit}/>
}

可以看出,一旦你处理了某种错误,那你就要层层上报,否则就会被当作正常情况继续执行

此外,还有个很严重的问题在于,我们无法感知错误的类型,我们无法从 interface 中得知这段程序会返回什么错误,我们很可能忘记处理错误,而且由于 JS 中只有 Error,我们想要定制其他的 Exception,那就只能去自己构造,但是最终还是要判断究竟是自己的业务抛出的错误,还是代码执行的错误,这个可能会更复杂;

小结

当然了,实际的开发中我们肯定是会结合两种形式去做取舍,但是我们应该熟悉这两种的适用场景,合适的使用可以使我们的代码更加的清晰简洁,也是一种进步吧