async, await and try catch
這篇想說一下async
﹑await
語法的一些小細節,首先從async
來說吧,一般來說,async function 是在內部有需要用await
等 Promise 結果的時候才使用,也由於這個特性,async function 的回傳值都會是個 Promise,意思就是你回傳非 Promise 的值,會自動被包成 Promise,所以像下面的程式:
async function wow () {
return Promise.resolve(100);
}
wow().then(v => { console.log(v); });
就等同於:
async function wow () {
return 100;
}
wow().then(v => { console.log(v); });
和直接回 Promise value 比起來,效能上不會有什麼顯著差異,從建議的實做方法來看就是多一個判斷。再來看看await
吧,首先一樣,await
一般是用來接 Promise 的,不過其實也是可以接非 Promise value 的
async function wow () {
var r = await 1;
console.log(1);
}
wow();
console.log(2);
所以這樣的程式碼也可以正確執行,不過 await 那邊的執行方式還是會維持非同步的(實際上應該是後面的東西都會用 Promise 包起來一次),所以這段程式碼的輸出會是先輸出2
再輸出1
。
再來這點可能比較多人知道,就是連續的多個await
不會讓這些非同步操作同時開始:
async function wow () {
const a = await fetch('/a');
const b = await fetch('/b');
const c = await fetch('/c');
return [a, b, c];
}
這樣其實三個請求會照順序執行,a
有結果了才去要b
,b
有結果了才去要c
,而不是同時處理,如果要同時發出請求則還是需要用Promise.all
,然後不用async
了:
function wow () {
return Promise.all([
fetch('/a'),
fetch('/b'),
fetch('/c')
]);
}
不要await
的話,也是可以先 assign 給變數的:
function wow () {
const a = fetch('/a');
const b = fetch('/b');
const c = fetch('/c');
return Promise.all([a, b, c]);
}
然後其實Promise.all
是要所有的 Promise 都 fulfilled 時才會 resolve,另外一個角度來看,就是其中只要一個 rejected 的話,就不會 resolve,實際上使用起來變化有點少,而且要做忽略錯誤的fetch
也有點麻煩,所以現在 TC39 還有個新的草案叫 Promise.allSettled,不管是 resolve 還是 reject,只要所有參數內的 Promise 都結束了,allSettled
就會 resolve,目前這草案還在 stage 1,過幾天的會議有望升到 stage 2,不過這是題外話。
最後一個想說的就是await
處理 rejected Promise 的問題,如果是從 jQuery 時期就開始寫 Deferred/Promise 的人,可能會很習慣的把 Promise 的兩種狀態拿來當成值的一部份,事實上這也是jQuery.ajax
的設計,如果用這種想法來寫await
接值的時候,就會覺得很難處理rejected
的狀態,因為要用try...catch
:
async function wow () {
try {
const a = await fetch('/a');
} catch (error) {
// deal with non-ok fetch
}
}
要這樣寫還不如用舊的.then
來接看起來還漂亮一點。不過實際上,這是錯誤的理解 Promise,Promise 不是用來取得兩種狀態用的,而是用來非同步取得單一個數值用的機制,而所謂rejected
的狀態,其實就是發生非預期狀況(unexpected exception)的情形,這也就是為什麼 ECMAScript 版的 Promise 是用throw Error
的方式來 reject Promise。
我一直覺得用 HTTP 請求來比較這兩種設計蠻好理解的,使用 jQuery 的ajax
,server 端回非 200 的 status 的話,就會被當成是錯誤,然後回傳的 Promise 就會被 reject,但是在使用 ECMAScript Promise 的 fetch 中,不管 server 端回應的 status code,fetch 都會 resolve,而會 reject 的情形,就只有網路有問題的時候,像是網路斷線、存取被拒絕(CORS)等完全碰不到遠端主機的情形,也就是對於一個 HTTP 請求來說,真正的非預期狀況,所以如果你有兩種狀況要處理,那應該是回傳值的一部份,後面再用if...else
來做分支。
回來看await
的使用,究竟應該什麼時候來用try...catch
呢,我自己有一個很簡單的初步判斷條件,就是這個取值的程式碼,如果不是非同步,沒有使用await
的話,你會不會用try...catch
包起來,不會的話,那改成非同步操作的程式碼應該也不用try...catch
。不過現實世界當然還是比較難一點,非同步的取值風險和狀況還是比較多的,例如fetch
遇到網路問題會 reject,但是還是需要處理這種狀況,不用try...catch
的話,怎樣寫比較好呢?我的想法是,用.then/catch
處理好需要處理的情形,然後把結果包起來傳回去,所以要處理fetch
的非預期狀況的話,就可以改成:
async function wow () {
const a = await fetch('/a').catch(error => {
return {
ok: false,
status: -1,
error: error,
};
});
if (a.status === -1) {
// exception error handling
}
}
這邊我設計成有非預期狀況時,status code 為-1
,並且把 error 資訊也傳回去,然後後面就可以直接拿來判斷是不是非預期狀況,當然也可以把這個處理包成一個自己的myFetch
:
const myFetch = (url, options) =>
fetch(url, options)
.catch(error => {
ok: false,
status: -1,
error: error,
});
然後原來的程式就可以直接拿myFetch
取代fetch
了。
如果要通用一點的,其實有一個叫 await-to-js 的套件我覺得蠻不錯的,直接拿官方的範例看吧:
import to from 'await-to-js';
async function asyncTaskWithCb(cb) {
let [err, user] = await to(UserModel.findById(1));
if (!user) return cb('No user found');
}
它可以包裝 Promise 物件,然後不管那個 Promise 成功還是失敗,它自己都會 resolve,resolve 的值就是[error, value]
這樣形式的陣列,一來符合 node 的 error-first callbacks,再來就是配合 destructuring assignment 其實程式碼是蠻漂亮的。