最近在思考错误的机制,通常我们有两种使用错误的方式,一种就是返回错误,另一种则是抛出错误
// 返回错误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(); // 弹窗开始加载显示loadingconst [error, res] = await onSubmit(values); // 提交数据modal.fail(); // 弹窗隐藏loadingif (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,那就只能去自己构造,但是最终还是要判断究竟是自己的业务抛出的错误,还是代码执行的错误,这个可能会更复杂;
当然了,实际的开发中我们肯定是会结合两种形式去做取舍,但是我们应该熟悉这两种的适用场景,合适的使用可以使我们的代码更加的清晰简洁,也是一种进步吧