上面提的的这些异常需要我们足够了解自己的程序,对这些可能会出问题的地方做异常捕,当这些异常发生时能返回给客户端一个“可控”的结果,也能让我们对自己的程序更可控。下面先来看看常见的异常。
const user = { name: 'max' } console.log(user.hello)没定义的东西不要乱取,取的时候也可以考虑用更安全的方式比如 lodash 的 get。
const data = getFromServer() data.user = 'abc'不要相信 server 的数据结构,先判断有没有 user,后端数据结构给错了是他的问题,但是程序因为这个崩了就是我们异常没处理的问题。
console.log(typeof null); // 输出 object console.log(typeof undefined); // 输出 undefinednull 和 undefined 的不要傻傻分不清楚, ?? 操作符是用来做什么的?
console.log("a" / 2); // 输出 NaN看到一些网站上经常出现 NaN,这也是计算杀手,必须判断数据是数字后再计算,不然 NaN 会给用户脑袋中出现一个大大的问号,包括 parseInt("abc") 输出是什么?
var arr = [1, 2, 3]; console.log(arr[3]); // 输出 undefined越界应该不能吧?
const fs = require('fs'); const write = function () { fs.mkdir('./writeFolder'); fs.writeFile('./writeFolder/foobar.txt', 'Hello World'); } write();这代码一定报错,fs.mkdir和 fs.writeFile 都是异步方法,第3行代码没执行完,第四条命令就开始运行了即目录都没有创建好,就开始往这个目录下的文件里写内容导致程序崩溃。怎么解?改成顺序调用?即创建目录后再调用写文件?
const fs = require('fs'); const write = function () { fs.mkdir('./writeFolder', ()=>{ fs.writeFile('./writeFolder/foobar.txt', 'Hello World'); }); } write();这代码还是有问题,因为你也不确定这个目录有没有创建成功,我们需要记住一点 Node.js 的异步函数都是遵从 Error irst 原则,即如果这个异步操作失败, 第一个参数就会返回 error 值。
const fs = require('fs'); const write = function (callback) { fs.mkdir('./writeFolder', (err, data) => { if (data) fs.writeFile('./writeFolder/foobar.txt', 'Hello World!'); else callback(err) }); } // 堆代码 duidaima.com write(console.log);这样就没问题了吗? 这个 write 函数还是有问题,我们只处理了 fs.mkdir 的错误,当 fs.writeFile 发生错误的话,我们的错误就被“吞掉了”。当然,我们的目标不是通过 if else 来处理 callback 的错误,而是把 callback 的异步方法封装成 promise,来使用更加语义和现代的 catch, 封装一个promise人人都会吧?不会的话参考这个例子封装个异步 sleep 函数练练手?
function doesWillNotAlwaysSettle(arg) { return new Promise((resolve, reject) => { doATask(foo, (err) => { if (err) { return reject(err); } if (arg === true) { resolve('I am Done') } }); }); }这里要提醒的是,千万别在第五行画蛇添足返回一个 字符串 类型,最佳实践是返回 error 对象 或者再 wrap 一个 error 对象,否则会 say goodbye to stack trace。
function readTemplate() { return new Promise(() => { databaseGet('query', function(err, data) { if (err) { reject('Template not found. Error: ', + err); } else { resolve(data); } }); }); } readTemplate();Promise Hell?
'use strict'; const fs = require('fs').promises; const write = function () { return fs.mkdir('./writeFolder').then(() => { fs.writeFile('./writeFolder/foobar.txt', 'Hello world!').then(() => { // do something }).catch((err) => { console.error(err); }) }).catch((err) => { // catch all potential errors console.error(err) }) }看着可怕不?怎么解决呢?用 async/await , 除了有这个好处外,还有一个好处!
const p = new Promise((resolve, reject) => { throw new Error('同步错误'); }); p.catch(error => console.log(error)); p.then(() => console.log('已解决'));
观察上面代码,那是不是第3行是 dead zone? resolve 后这个 promise的状态已经变了,无法再改变。所以写代码时候不要搞这种心智负担,直接 try catch,通过将同步代码包装在 try/catch 块中,我们可以在出现任何错误的情况下正确拒绝 promise。这确保了所有错误(同步和异步)都得到正确处理。
router.get('/:id', function (req, res, next) { database.getData(req.params.userId) .then(function (data) { if (data.length) { res.status(200).json(data); } else { res.status(404).end(); } }) .catch(() => { log.error('db.rest/get: could not get data: ', req.params.userId); res.status(500).json({error: 'Internal server error'}); }) });像这个代码就问题很大,直接返回 500 服务器异常,太不友好了!你返回 400,给出错误原因,客户端知道是他自己的问题,500 会给所有人带来“沟通的痛苦”
async function foobar() { throw new Error('foobar'); } async function baz() { throw new Error('baz') } (async function doThings() { const a = foobar(); const b = baz(); try { await a; await b; } catch (error) { // ignore all errors! } })();咋办?上Promise all呗
(async function doThings() { const a = foobar(); const b = baz(); try { await Promise.all([a, b]); } catch (error) { // ignore all errors! } })();还有一种情况非常容易出现 unhandled rejections
async function foobar() { throw new Error('foobar'); } async function doThings() { try { return foobar() } catch { // ignoring errors again ! } } doThings();
第七行明显没有处理错误,因为没 await,没明白? 看这篇 你知道吗?在try/catch块之外,return await是多余的。
class ApplicationError extends Error { get name() { return this.constructor.name; } } class DatabaseError extends ApplicationError { } class UserFacingError extends ApplicationError { } module.exports = { ApplicationError, DatabaseError, UserFacingError }有了这 3 个类以后,我们就可以开始处理具体异常了,举一个用户查询不到的异常吧,其他异常大家可以自己发散,(比如连接数据库挂了,call第三方接口挂了,代码runtime挂了,各种)。
class NotFoundError extends UserFacingError { constructor(message, options = {}) { super(message); // You can attach relevant information to the error instance // (e.g.. the username) for (const [key, value] of Object.entries(options)) { this[key] = value; } } get statusCode() { return 404 } }接下来就把我们定义的错误 用起来
app.get('/:id', async function (req, res, next) { let data try { data = await database.getData(req.params.userId) } catch (err) { return next(err); } if (!data.length) { // 我们用刚才定义的类来处理 用户没查到异常 return next(new NotFoundError('Dataset not found')); } res.status(200).json(data) }) app.use(function (err, req, res, next) { // 堆代码 duidaima.com // 判断是哪一类错误 if (err instanceof UserFacingError) { res.sendStatus(err.statusCode); res.status(err.statusCode).send(err.errorCode) } else { res.sendStatus(500) } // 必须 log 异常 logger.error(err, 'Parameters: ', req.params, 'User data: ', req.user) });
这里有2个建议,demo 这么写 ok,但是工程里要分层处理错误。如果在任何地方处理错误,那么对错误处理的方法就是不一致的,代码又乱又难以追踪,其次就确保你抛出的错误类是你唯一异常原因来源,并且包含了调试应用程序所需的所有信息,不然你的错抛出来也无法定位到到问题,由于你的异常原因不准确不唯一。