如何优雅地在Node应用中进行错误处理

不知道你有没有遇到这样一种状况,某天你写的代码在线上忽然发生错误,而后你打开控制台,却对着打过包的错误信息毫无头绪?又或者说是代码在node端出现了问题,你查看错误日志的时候,却发现日志文件中都是杂乱的错误堆栈信息。javascript

其实上面这些问题均可以经过在代码中引入合适的错误机制进行解决。大部分时候,因为程序员在开发过程当中更加关注需求的实现,反而会忽视一些底层的工做。而错误处理机制就至关于咱们代码上的最后一道保险,在程序发生已知或者意外的问题的时候,可让开发者在第一时间获取信息,从而快速定位并解决问题。前端

经常使用的错误处理机制

首先咱们来了解一下目前前端领域到底有哪些错误处理机制。java

try catch

try...catch这种错误处理机制必定是你们最熟悉的,Javascript语言内置的错误处理机制能够在检测到代码异常的时候直接进行捕获并处理。node

function test() {
  try {
    throw new Error("error");
  } catch(err) {
    console.log("some error happened:");
  }
}

test()
复制代码

node原生错误处理机制

大多数Node.js核心API都提供的是利用回调函数处理错误,例如:git

const fs = require('fs');

function read() {
	fs.readFile("/some/file/does-not-exist", (err, data) => {
    if(err) {
      throw new Error("file not exist");
    }
    console.log(data);
  });
}

read();
复制代码

经过回调函数的err参数来检查是否出现错误,再进行处理。之因此Node.js采用这种错误处理机制,是由于异步方法所产生的方法并不能简单地经过try...catch机制进行拦截。程序员

promise

Promise是用于处理异步调用的规范,而其提供的错误处理机制,是经过catch方法进行捕获。github

fs.mkdir("./temp").then(() => {
	fs.writeFile("./temp/foobar.txt", "hello");
}).catch(err => {
	console.log(err)
});
复制代码

async/await + try catch

第三种错误处理机制是采用async/await语法糖加上try...catch语句进行的。这样作的好处是异步和同步调用都可以使用统一的方式进行处理了。typescript

async function one() {
	await two();
}

async function two() {
	await "hello";
	throw new Error("error");
}

async function test() {
	try {
		await one();
	} catch(error) {
		console.log(error);
	}
}

test();
复制代码

解决方案

promisify

若是你的代码中充斥着多种不一样的错误处理模式,那么维护起来是十分困难的。并且代码的可读性也会大大下降。所以,这里推荐采用的统一的解决方案。对于同步代码来讲,直接使用try...catch方式进行捕获处理便可,而对于异步代码来讲,建议转换成Promise而后采用async/await + try...catch这种方式进行处理。这样风格统一,程序的健壮性也大大增强。例以下面这个数据库请求的代码:数据库

const database = require("database");

function promiseGet(query) {
    return new Promise((resolve, reject) => {
        database.get(query, (err, result) => {
            if (err) {
                reject(err);
            } else {
                resolve(result);
            }
        })
    })
}

async function main() {
    await promiseGet("foo");
}

main();
复制代码

自定义错误类型

直接使用系统原生的错误信息一般会显得太过单薄,不利于后续进一步的分析和处理。因此为了让代码的错误处理机制的功能更增强大,咱们势必要多花点精力进行额外的改造。express

能够经过扩展基础的Error类型来达到这一目的。

通常来讲,要根据错误发生的位置采用不一样的错误类型。

首先是应用层错误,它会保存额外的线索数据:

class ApplicationError extends Error {
  constructor(message, options = {}) {
    assert(typeof message === 'string');
    assert(typeof options === 'object');
    assert(options !== null);
    super(message);

    // Attach relevant information to the error instance
    // (e.g., the username).
    for (const [key, value] of Object.entries(options)) {
      this[key] = value;
    }
  }

  get name() {
    return this.constructor.name;
  }
}
复制代码

接着,能够再定义用户接口的错误类型,该类型主要用于直接返回给客户端,好比错误状态码等等。

class UserFacingError extends ApplicationError {
	constructor(message, options = {}) {
		super(message, options);
	}
}

class BadRequestError extends UserFacingError {
  get statusCode() {
    return 400
  }
}

class NotFoundError extends UserFacingError {
  get statusCode() {
    return 404
  }
}
复制代码

另外,对于底层的数据库错误来讲,可能须要更加细节的错误信息。此时也能够根据业务须要进行自定义:

class DatabaseError extends ApplicationError {
  get toString() {
    return "Errored happend in query: " + this.query + "\n" + this.message;
  }
}

// 使用的话
throw new DatabaseError("Other message", {
  query: query
});
复制代码

化繁为简,集中处理

express

有了基础的错误数据类型后,咱们能够在代码里针对不一样的错误类型采起不一样的解决方案。 接下来,以Express应用为例讲解一下使用方法。

app.use('/user', (req, res, next) => {
  const data = await database.getData(req.params.userId);
  if (!data) {
    throw new NotFoundError("User not found")
  }
  
  // do other thing
});

// 错误处理中间件
app.use(async (err, req, res, next) => {
  if (err instanceof UserFacingError) {
    res.sendStatus(err.statusCode);
    // or
    res.status(err.statusCode).send(err.errorCode)
  } else {
    res.sendStatus(500)
  }
  
  // 记录日志
  await logger.logError(err, 'parameter:', req.params, 'User Data:', req.user);
	// 发送邮件
  await sendMailToAdminIfCritical();
})
复制代码

具体到实际场景中,须要在不一样的路由中抛出不一样的错误类型,而后咱们就能够经过在错误处理中间件中进行统一的处理。好比根据不一样的错误类型返回不一样的错误码。还能够进行记录日志,发送邮件等操做。

database

数据库发生错误的时候,除了常规的抛出错误,有时候你可能还须要进行额外的重试或回退操做,如:

// 发生网络错误的时候隔200ms,重试3次
function query(queryStr, token, repeatTime = 0, delay = 200) {
  try {
    await db.query(queryStr);
  } catch (err) {
    if (err instanceof NetworkError && repeatTime < 3) {
      query(queryStr, token, repeatTime + 1, delay);
    }
    
    throw err;
  }
}
复制代码

未处理错误

对于未处理的错误来讲,咱们可使用node.js的unhandledRejection事件进行监听:

process.on('unhandledRejection', error => {

  console.error('unhandledRejection', error);
	// To exit with a 'failure' code
  process.exit(1);
});
复制代码

并且从Node.js 12.0开始,可使用如下命令启动程序:

node app.js --unhandled-rejections
复制代码

这样也可以在发现未处理异常的时候进行处理,官方支持了三种对应的处理模式:

strict: Raise the unhandled rejection as an uncaught exception.

warn: Always trigger a warning, no matter if the unhandledRejection hook is set or not but do not print the deprecation warning.

none: Silence all warnings.

总结

最后,总结一下。为了实现可扩展和可维护的错误处理机制,咱们能够须要注意如下几个方面:

  • 使用自定义Error类,后续还能根据业务须要进行扩展
  • 将异步代码转换成Promise,而后统一使用async/await + try...catch的形式进行错误捕获
  • 尽可能采用统一的错误处理中间件函数
  • 保持Error信息可理解,返回合适的错误状态和代码
  • 对于未处理的错误,要即便捕获并记录

——转载请注明出处———

微信扫描二维码,关注个人公众号

最后,欢迎你们关注个人公众号,一块儿学习交流。

参考资料

youtu.be/ArfAzp_bSq4 softwareontheroad.com/error-handl… github.com/goldbergyon… michalzalecki.com/an-elegant-… khalilstemmler.com/articles/en…

相关文章
相关标签/搜索