nodejs中如何使用throw进行异常处理 ?

5 阅读5分钟

为什么会有这样的问题?

是因为之前都是写前端项目,最近在写一个nodejs项目时在思考一个问题: 在方法调其他方法的过程中,其他方法是采用return来返回特定格式的错误数据还是使用throw来直接抛出错误?

使用return来返回特定格式的错误数据

用例子来解释一下,如下代码所示:

// 一个校验数据的方法(采用return来返回特定格式数据的方式)
const validateData = (data) => {
    if (!data.name) {
        return { pass: false, message: '名称不能为空' }  
    }
}

// 一个提交数据的方法,在其中调用校验方法
const submit = (data) => {
    const res = validateData(data);
    if (!res.pass) {
        // 提示用户错误信息
        alert(res.message);
    }
}

submit({ name: '' });

在上面的代码中,validateData方法使用return返回特定格式的数据,在提交数据之前调用validateData方法校验数据,校验不通过将错误信息提示给用户。

上面这个例子是在前端项目中经常会遇到的一种情况,但是在后端项目中,重点并不是提示用户错误信息,而是在接口中以特定格式返回错误信息,例如:

// 一个校验数据的方法(采用return来返回特定格式数据的方式)
const validateData = (data) => {
    if (!data.name) {
        return { pass: false, message: '名称不能为空' }  
    }
}

// 一个在数据库表中新增一行数据的方法,在其中调用校验方法,return返回的数据将会作为接口的返回数据
const addRow = (data) => {
    const res = validateData(data);
    if (!res.pass) {
        return { code: -1, message: res.message }
    }
    // 数据库中新增数据
}

addRow({ name: '' });

在上面的代码中,通过两层return来返回数据;在只有两层时还好维护这样的代码,但是当后端代码处理的业务非常复杂,方法调用层级非常深后就会变得难以维护,因为每一层方法都要判断下一层的返回然后再处理结构后返回给上一层。那我就在想如何更简单呢?想到了直接使用throw来抛出错误。

使用 throw 来抛出错误

在后端项目代码中,主要是处理业务逻辑然后将结果通过接口返回,不像前端项目代码中有一部分业务逻辑还有用户交互。所以完全可以添加一个中间件,处理代码中抛出的所有错误然后封装后返回给接口调用方。例如在nodejs项目中:

// app.js文件
import 'express-async-errors'; // 引入这个模块后能捕获未被处理的异步报错并传递给错误处理中间件
import express from 'express';
import routes from './routes';
const app = express();
// 一次性挂载所有路由
app.use(routes);
// 全局错误处理中间件,所有未捕获的报错都在此处理,业务代码专注于业务逻辑即可
app.use((error, req, res, next) => {
  // 统一处理所有没有被捕获的报错
  res.status(200).json({ 
      code: -1, 
      message: error?.message || '服务器错误!', 
      data: null 
  });
});


// routes.js文件
import express from 'express';
const router = express.Router();
// 一个校验数据的方法(采用thorw来抛出错误的方式)
const validateData = (data) => {
    if (!data.name) {
        throw new Error('名称不能为空'); 
    }
}
// 一个在数据库表中新增一行数据的方法,在其中调用校验方法
const addRow = (data) => {
    // 校验数据
    validateData(data);
    // 数据库中新增数据
}
// 定义了一个路径为 /addRow 的路由,调用了 addRow 方法
router.get('/addRow', (req, res) => {
    addRow({ name: '' });
})

这样,不论是在多深层级的方法中用throw抛出一个错误,只要没有被捕获,都能在最外层处理。

为什么要“用throw抛出错误然后在全局错误处理中间件中处理”这样去做?

因为在后端业务逻辑复杂的情况下,可能会存在很多层级的代码调用,全部使用return来返回特定格式数据在编写和维护时较为复杂,而使用throw来抛出后统一处理有这些好处:

  1. 统一处理错误,让代码编写和维护变得更简单

    • 统一处理错误:通过throw抛出错误并在全局错误处理中间件中处理,能够将所有类型的错误(如路由处理中的错误、数据库操作错误、第三方 API 调用错误等)集中到一个地方进行处理;这能够让错误处理逻辑更加清晰和有条理,避免了错误处理零散的分布在各个模块方法中。
    • 编写维护简单:在深层级的方法中编写代码不用考虑错误情况下的数据返回,直接抛出错误即可;所有的错误处理逻辑都在一个地方,后续有错误逻辑的更新只需要修改这一个地方即可,便于维护。
  2. 增强程序的稳定性

    • 防止程序崩溃:在某个方法或者模块中发生错误时,如果没有处理,可能会导致整个程序崩溃;通过全局错误处理中间件来处理所有未被捕获的错误,能够避免单个错误引发的程序崩溃终止。
  3. 一致的用户体验

    • 统一的错误信息数据结构:在全局错误处理中间件中可以统一错误信息的数据结构后返回给客户端。这样客户端代码能够更好的处理错误将错误展示给用户。
    • 清晰的错误提示:针对一些错误码的报错,可以将其转化为用户能够理解的文字提示后返回给客户端。