现在node.js一般使用async/await来处理回调嵌套地域。然后使用try/catch来捕获错误。
try/catch的好处
比如下面的例子,当不传参数时回调函数会抛出异常:
const util = require('util')
//传统回调函数
function task(...args) {
//最后一个参数一般为回调,将其从数组中移出
let cb = args.pop()
//无参数时抛出错误
if (args.length < 1) {
cb(new Error('Wrong parameter'))
return
}
//error first写法,没有错误时第一个参数传null
cb(null, args)
}
//转化为promise方法,以便使用async/await
const taskAsync = util.promisify(task)
//声明一个async的箭头函数
;(async()=>{
try {
let [arg1, arg2] = await taskAsync(1, 2)
await taskAsync()
} catch(err) {
console.log(err)
}
})()
使用try/catch的好处是可以将很多可能的错误代码包在一个块中,然后只使用一个异常捕获。并且第一条执行语句并不再需要接收处理error。
但现实生活中,基本上需要对每一处异常需要单独处理,这样使用try/catch的代码就会显得非常臃肿,如 Dima 在 https://blog.grossman.io/how-to-write-async-await-without-try-catch-blocks-in-javascript/ 这篇博文中的例子:
async function asyncTask(cb) {
try {
const user = await UserModel.findById(1);
if(!user) return cb('No user found');
} catch(e) {
return cb('Unexpected error occurred');
}
try {
const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});
} catch(e) {
return cb('Error occurred while saving task');
}
if(user.notificationsEnabled) {
try {
await NotificationService.sendNotification(user.id, 'Task Created');
} catch(e) {
return cb('Error while sending notification');
}
}
if(savedTask.assignedUser.id !== user.id) {
try {
await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
} catch(e) {
return cb('Error while sending notification');
}
}
cb(null, savedTask);
}
这样的代码就显得过长
将error变成返回参数
上面的博文中提到,go语言使用类似javascript error first的原则,将异常变成了一个参数:
data, err := db.Query("SELECT ...")
if err != nil { return err }
博客中将promise方法进行了封装,将error返回成第一个参数:
// to.js
export default function to(promise) {
return promise.then(data => {
return [null, data];
})
.catch(err => [err]);
}
然后上面那段很多try/catch的代码就会转换成:
import to from './to.js';
async function asyncTask() {
let err, user, savedTask;
[err, user] = await to(UserModel.findById(1));
if(!user) throw new CustomerError('No user found');
[err, savedTask] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
if(err) throw new CustomError('Error occurred while saving task');
if(user.notificationsEnabled) {
const [err] = await to(NotificationService.sendNotification(user.id, 'Task Created'));
if (err) console.error('Just log the error and continue flow');
}
}
结构上简洁了很多,但每一处await都多了一个中间方法to的调用。
方法改进
Dima 的方法其实可以稍微改进一下,我们可以借鉴util.promisify方法,将普通函数封装一下,做一个通用方法,将普通回调转化成将error始终放到第一个返回参数的promise方法:
const util = require('util')
function task(...args) {
let cb = args.pop()
if (args.length < 1) {
cb(new Error('Wrong parameter'))
return
}
cb(null, args)
}
//将参数函数转化成error first的promise方法
function promisify(fn) {
let fnAsync = util.promisify(fn)
return (...args)=>{
return fnAsync(...args)
.then(data=>{
return [null, data]
})
.catch(err => [err])
}
}
const taskNoTry = promisify(task)
;(async()=>{
var [ err, args ] = await taskNoTry()
if (err) return console.log(err)
//数组参数解构
var [ err, [arg1, arg2] ] = await taskNoTry(1, 2)
if (err) return console.log(err)
console.log(arg1, arg2)
})()
这种方法的一个缺陷是error放到了返回数组中,对数据解构略显奇怪。
666