使用抛异常的方式返回Nodejs校验不通过的结果是不是一个好的处理方式?

发布于 1周前 作者 zlyuanteng 来自 nodejs/Nestjs

我正在写一个 Express 服务,其中有个地方需要校验用户的输入。我的处理是在校验不通过后抛出一个异常,由全局的异常处理来返回 400 响应。我的同事觉得我这种处理方式不行,他的理由如下:

  • try catch 会增加性能开销
  • exception 应该用于 error handling

我觉得有些道理,但是我之前写 Java 的时候看到过很多这种用法,而且我觉得这种用法写起来也比较方便。难道这真的不是一个好的处理方式?


使用抛异常的方式返回Nodejs校验不通过的结果是不是一个好的处理方式?

32 回复

Nodejs 不知道,Java 的确挺常见的,相比于增加的一点性能开销,try catch 带来的好处也不少,这种情况往上抛异常一般表示这个异常你不需要处理,往上抛上层自然有别的方法来处理,一层层往上走即可,直到抛到顶层。如果你自己去处理这套逻辑,可能比较麻烦,容易出错,而且为了这点性能,真的有必要这样作么?这是个平衡问题,不知道同样道理适不适用于 nodejs


绝大多数网站没有到需要考虑 try catch 性能开销的用户量。

是。他的这两个理由都很牵强

非常合理,这类目的直接返回简单 400 回应的用户输入异常就是 exception 。

除非是表单校验之类预期用户会输入不合理数据并要给出相应提示的情况下不用异常来处理也合理。

理解不一样吧,try catch 倾向于程序处理中的未知错误,表单验证是 if else 能处理的。他是不是 C/Go 等语言过来的😂

我理解 try…catch 是处理逻辑异常,TypeError 、BoundsError 也是逻辑上的异常,逻辑上传错了类型,接收错了类型。

程序错误,抛出异常给 500 响应,逻辑异常给 400 响应没毛病

验证不通过其实就是输入错误难道不是 error 不应该走 error handling 么?谈 try catch 会增加性能开销那更是扯淡,不是说没有性能开销而是实际业务中纠结这个毫无意义

以前我指向返回 Result 带 Success 与否,现在我直接抛异常,异常类型统一定义方便统一截获处理。比如数据验证异常就可以友好返回。

好处是代码写起来很简洁,Fail First 。

动不动就性能开销,cpu 和 内存发展的那么快不就是给你开销的么。都不开销,放着看么

try catch 没有开销,跟你正常判断一样。

如果 “ try catch 会增加性能开销” 有道理, 那不止 Java , 大部分语言都要回炉重造。不过这个确实看语言框架。
“ exception 应该用于 error handling”,这就是纯粹的胡说八道,用户输入校验不通过,这不是错误,那系统就没有错误了。当然,既然用了 exception ,那就得什么样的错误配什么种类的 exception ,别无脑什么错误都抛出同一种 exception(msg )

Java 基本也只能这么做吧

用 Java 、Node.js 不需要考虑性能问题,大多采取微服务集群的方式就可以消化并发负载,真的是单个请求有性能问题,应该在有问题的这部分使用 C++、Rust 等语言使用高性能方案处理。

个人认为关于代码,最重要的是可读性,其次是可扩展性,最后才是性能。

异常会有很多种,有的需要返回 400 ,有的需要返回 401 ,有的需要返回 403 ,等等。如果你定义好了不同类型的 error ,然后可以根据 error 类别来精准返回错误信息,且同类 error 的返回信息在未来有可能会统一修改,那么全局处理可能会让代码更简洁、清晰一些。你只需要处理好 error 的 stack ,确保未被捕获的 error 可以明确找到真实发生的代码位置就行。

如果你的信息返回格式是碎片化的,那么还不如每个接口自行组织和返回信息。

当然通常肯定都是部分状态可以统一处理,部分状态需要分别处理,那么只需要把适合统一处理的放到全局处理就好了。

没有万能的设计,都是要根据项目当前的需求特点和未来可能的需求来选择采取哪种设计方式。

接口里调用了 N 个方法,在最后一个方法里 raise 异常直接返回前段,不要太舒服

相当于讨论"微服务之间的 http 调用是否应该捕获所有异常,然后返回 http status 200,最后在 response body 里面返回异常码和异常信息。"

正方和反方只能说各执一词吧。

错误和异常确实要分开处理,比如异常应当越过多级调用直接到最上层,错误要调用层单独处理、错误是普通返回但异常要报警通知、异常返回给用户的信息要脱敏或者简单替换为系统繁忙,等等
很多人嘲笑 go 的 if err ,我认为这是正确处理错误和异常无法避免的操作

这个说法确实没有问题

不过,在业务代码比较复杂的情况下,抛出异常而不是使用统一返回会让代码更简单易读

如果层级不深,或者这个错误很少出现,使用抛异常方式会简单很多。像某些参数校验接口,错误次数一多,内存占用能翻倍,很容易就会 OOM

身为一名 Java 或者说 Kotlin 用户,我的观念是这样的:
从需求本质看,我们需要的是对一个函数能清晰地得到它的正确状态下的返回值,和可能的失败情况。

那么,Java 的 try catch 是一个比较好的方案,它能不影响正确状态的返回值,并且错误状态能携带额外信息。

而另一种传统的错误码形式,它的返回更冗余,并且还需要对着看,想附加额外的错误信息并不那么方便。简而言之,表达力不够强。

嗯,所以我认为 Result 是最理想最兼顾的方案。关于这点的讨论可以参考: https://www.zhihu.com/question/330263279

理由太牵强,Nodejs 也可以这样使用,主要是风格/习惯问题

Nodejs 可以使用 https://github.com/jshttp/http-errors

其它我不知道,我一直这么写爽是真的爽。全局捕获就完事了

都这么处理的吧 底层框架很多也这么写的

try catch 确实会会增加一些开销,主要是在 java.lang.Throwable#fillInStackTrace()中会去爬取堆栈,但参数校验这个场景下不用纠结—抛异常是综合起来性价比最高的方式,jdk 其实有 java.lang.Throwable#Throwable(java.lang.String, java.lang.Throwable, boolean, boolean)这样一个构造器去构造不需要爬栈的异常(部分场景下只依赖异常类型而不关心堆栈),但不知道为什么并没有开放出来,后续如果能开放出来搭配这个场景应该是最优解。

try catch 可能不是性能洼地

据我所知 try catch 的代码在指令层面每执行一条就要去检查状态寄存器异常标志位是否被设置。我觉得这个开销其实还好。

我感觉这两个理由实在是太牵强了。我们公司就是“使用抛异常的方式返回校验不通过的结果”,自定义了一个 businessException ,入参是错误码,由全局异常处理来根据错误码返回对应的错误信息给用户。

明明是 golang 的 if err 更适合错误处理、鲁棒性,一帮习惯了 java 的人喷 golang if err 。

好歹换成 Haskell 吧, 21 世纪了还在用 sum type 代替 product type

好用,但是如果属于经常触发,还是建议有替代方案

后端处理不了用户请求,可不就是遇到了异常情况。很多表单验证的库,验证不通过也是抛异常出来的。当然他们也有可选用 boolean 来标记是否通过验证。
性能问题基本可以忽略不计吧。如果是牺牲可读性来换这点性能,那咋还用 js 呢,不得换语言先。
我认为抛异常还是手动判断只是风格问题,他说的理由我感觉就是在扯淡。

我是主张用异常的。
如果为了性能,自定义的业务异常类可以避免捕获调用栈,生成调用栈的性能开销是极大的。

在Node.js中,使用抛异常(throw)来处理校验不通过的结果是一种可行的方案,但是否是一个好的处理方式取决于具体的使用场景和上下文。

优点

  1. 代码简洁:抛异常可以使得代码更加简洁,校验失败时直接抛出异常,避免使用复杂的条件判断。
  2. 易于调试:异常可以被捕获并记录详细的错误信息,便于调试和日志记录。

缺点

  1. 性能影响:频繁地抛出和捕获异常会对性能产生影响,尤其是在高并发场景下。
  2. 不符合常规逻辑:异常通常用于处理不可预见的错误,而校验失败往往是可预见的,使用异常可能不符合常规的逻辑处理方式。

示例代码

function validateInput(input) {
    if (!input || typeof input !== 'string') {
        throw new Error('Invalid input: Input must be a non-empty string');
    }
    // 进一步校验逻辑
    return true;
}

try {
    const isValid = validateInput(null);
} catch (error) {
    console.error('Validation failed:', error.message);
}

建议

对于校验不通过的结果,更推荐的方式是使用返回值或回调函数来传递错误信息。这种方式更符合常规逻辑,且不会对性能产生显著影响。

function validateInput(input, callback) {
    if (!input || typeof input !== 'string') {
        callback(new Error('Invalid input: Input must be a non-empty string'));
    } else {
        callback(null, true);
    }
}

validateInput(null, (err, isValid) => {
    if (err) {
        console.error('Validation failed:', err.message);
    } else {
        console.log('Validation succeeded');
    }
});

综上所述,是否使用抛异常的方式取决于具体需求和场景。

回到顶部