此文爲轉載的,感謝原文作者——syaning!
一、引言
一般情況下,我們會使用try..catch..來進行異常處理,例如:
function sync() {
throw new Error('sync error');
}
try {
sync();
} catch (err) {
console.log('error caught:', err.message);
}
// error caught: sync error
然而對於異步函數,try..catch..就無法得到想要的效果了,例如:
function async() {
setTimeout(function() {
throw new Error('async error');
}, 1000);
}
try {
async();
} catch (err) {
console.log('error caught:', err.message);
}
// Uncaught Error: async error
這是因爲異步調用是立即返回的,因此當發生異常的時候,已經脫離了try..catch..的上下文了,所以異常無法被捕獲。
二、實現
下面來介紹異步編程下幾種常用的異常處理方法。
1. callback
通過回調函數可以比較方便地進行異常處理,例如:
function async(callback, errback) {
setTimeout(function() {
var rand = Math.random();
if (rand < 0.5) {
errback('async error');
} else {
callback(rand);
}
}, 1000);
}
async(function(result) {
console.log('scucess:', result);
}, function(err) {
console.log('fail:', err);
});
這裏通過setTimeout來模擬實現了一個異步函數async,該函數可能調用成功,也可能調用失敗拋出異常。async接收兩個參數,當調用成功時callback會被執行,當拋出異常時errback會被執行。
有時候爲了方便,也會將callback和errback合併爲一個回調函數,這也是Node風格回調處理。代碼如下:
function async(callback) {
setTimeout(function() {
var rand = Math.random();
if (rand < 0.5) {
callback('async error');
} else {
callback(null, rand);
}
}, 1000);
}
async(function(err, result) {
if (err) {
console.log('fail:', err);
} else {
console.log('success:', result);
}
});
這裏err作爲回調函數的第一個參數,如果async調用成功,則err爲一個falsy值(可以是null或undefined等,在該例子中使用null);如果async調用失敗,則err爲拋出的異常。當調用回調函數的時候,先判斷err是否爲falsy。如果爲falsy,則執行成功的回調;否則進行異常處理(這裏原文是錯誤的,剛好邏輯相反了,我估計是作者不小心寫錯了)。
然而在多異步串行的情況下,使用回調函數的方式,就會出現所謂的回調金字塔問題,代碼可讀性也會大打折扣。例如:
function async(callback) {
setTimeout(function() {
var rand = Math.random();
if (rand < 0.2) {
callback('async error');
} else {
callback(null, rand);
}
}, 1000);
}
async(function(err, result) {
if (err) {
console.log('fail:', err);
} else {
console.log('success:', result);
async(function(err, result) {
if (err) {
console.log('fail:', err);
} else {
console.log('success:', result);
async(function(err, result) {
if (err) {
console.log('fail:', err);
} else {
console.log('success:', result);
}
});
}
});
}
});
在這裏,回調函數一層嵌一層,而且每一層都要判讀是否出錯。不僅代碼可讀性差,維護起來也非常不方便。
2. promise
上面的例子,使用promise的話,代碼如下:
function async() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
var rand = Math.random();
if (rand < 0.5) {
reject('async error');
} else {
resolve(rand);
}
}, 1000);
});
}
async().then(function(result) {
console.log('success:', result);
}, function(err) {
console.log('fail:', err);
});
或者使用catch的方式,代碼如下:
function async() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
var rand = Math.random();
if (rand < 0.5) {
reject('async error');
} else {
resolve(rand);
}
}, 1000);
});
}
async().then(function(result) {
console.log('success:', result);
}).catch(function(err) {
console.log('fail:', err);
});
使用promise的好處是執行流程直觀,但是理解起來比回調函數要麻煩一些。
對於多異步操作串行的問題,使用promise的方式會使得代碼簡潔優雅,可讀性也很強。代碼如下:
function async() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
var rand = Math.random();
if (rand < 0.2) {
reject('async error');
} else {
resolve(rand);
}
}, 1000);
});
}
function onResolved(result) {
console.log('success:', result);
}
function onRejected(err) {
console.log('fail:', err);
}
async().then(onResolved)
.then(async)
.then(onResolved)
.then(async)
.then(onResolved)
.catch(onRejected);
3. domain
在Node中,有一個domain模塊(在io.js中該模塊已經標記爲deprecated),可以用來處理異步操作異常。示例代碼如下:
var domain = require('domain');
function async(callback) {
setTimeout(function() {
var rand = Math.random();
if (rand < 0.5) {
throw 'async error';
} else {
callback(rand);
}
}, 1000);
}
var d = domain.create();
d.on('error', function(err) {
console.log('fail:', err);
});
d.run(function() {
async(function(result) {
console.log('success:', result);
});
});
Domain類繼承自EventEmitter,所以它本質上就是一個事件發生器。