はじめに
SalesforceのLWC開発をしてると、必ずと言っていいほど変な非同期処理の書き方をしてる人がいるので、まとめてみた。
「いやいやこんなの自分の周りじゃ見ないよ」という人は環境に恵まれているのだと思う。
筆者もそこまでJSに明るいわけではないので、あくまで初歩的な話しかしない。
その1 Promise地獄
名前の通り。
Promise地獄という名称が一般的なのか知らないが、要はコールバック地獄がそのままthenで再現されたような、多重ネスト式になっている実装。
一応正常な実装と同じ挙動を取るのが逆に質が悪い。
コードレビューの段階でレビュアーもJSに疎い人がやってると後続のテスト工程とかで検知されずにそのまま本番まで行ってしまい、気付いた時には「既に本番で動いてるのでなるべく変えたくない」と修正が困難に。
結果後からプロジェクトに参画した者はこの読みにくいコードを必死こいて解読する苦行が最初の仕事となる。
あとこの書き方だと通常のPromiseチェーンと異なり必ずcatchを各thenごとに用意しなければならないが、これをやる人はたいていそんなこと知ったこっちゃないとcatchが足りてないか、最悪そもそもcatch自体を書いていないことも多い。
その場合、正常系はともかく当然異常系でボロボロNGが出まくる(ちゃんと異常系もテストしてればだが......)。
async function asyncFunc1() {
return 1;
}
async function asyncFunc2(arg1) {
return arg1 + 1;
}
async function asyncFunc3(arg2) {
return arg2 + 1;
}
asyncFunc1().then((result1) => {
console.log(result1); // 1
asyncFunc2(result1).then((result2) => {
console.log(result2); // 2
asyncFunc3(result2).then((result3) => {
console.log(result3); // 3
}).catch((error3) => {
console.error(error3);
});
}).catch((error2) => {
console.error(error2);
});
}).catch((error1) => {
console.error(error1);
});
上記の例を一般的なPromiseチェーンの書き方に直すと以下のようになる。
ネストが解消され、catchの数も減り、パッと見で読みやすくなったのではないだろうか。
Promiseチェーンの名前通り、前の非同期関数の返すPromiseをthenで解決、関数の戻り値をthen内で編集後returnし、それを次の非同期関数に渡している。
このように非同期関数を連鎖させて使うのが本来のthen-catchの使い方である。
またこちらの書き方の場合、catchは基本1つだけでよく、この単一のcatchがチェーンされた非同期関数すべての例外を捕捉してくれる(複数用意してやることも可能だが、基本は不要だろう)。
async function asyncFunc1() {
return 1;
}
async function asyncFunc2(arg1) {
return arg1 + 1;
}
async function asyncFunc3(arg2) {
return arg2 + 1;
}
asyncFunc1().then((result1) => {
console.log(result1); // 1
return result1;
}).then(asyncFunc2).then((result2) => {
console.log(result2); // 2
return result2;
}).then(asyncFunc3).then((result3) => {
console.log(result3); // 3
}).catch((error) => {
console.error(error);
});
その2 cacthのないPromiseチェーン
これも名前通り。
Promiseチェーン使うならcatchはマスト。
実装してるのが共通関数とかで、呼び出し元に例外を投げたいとかならcatchの中でthrowするべき。
async function asyncFunc1() {
return 1;
}
async function asyncFunc2(arg1) {
return arg1 + 1;
}
async function asyncFunc3(arg2) {
return arg2 + 1;
}
asyncFunc1().then((result1) => {
console.log(result1); // 1
return result1;
}).then(asyncFunc2).then((result2) => {
console.log(result2); // 2
return result2;
}).then(asyncFunc3).then((result3) => {
console.log(result3); // 3
});
Good Practiceはその1と同じ(必要に応じてcatchの中でthrowなりする)。
その3 コールバック地獄
setTimeoutなどのコールバック関数を逐次実行したいときに、コールバック地獄を書く人がいるが、new Promiseを使えば綺麗に書ける。
function callbackHell() {
setTimeout(() => {
const result1 = 1;
console.log(initialData);
setTimeout(() => {
const result2 = result1 + 1;
console.log(result2);
setTimeout(() => {
const result3 = result2 + 1;
console.log(result3);
}, 500);
}, 1000);
}, 1500);
}
callbackHell();
自分の場合はよく使うコールバック関数(大体setTimeout)をこんな感じにnew Promiseでラップして返す共通関数を作り、それを使いまわすようにするが、そこら辺は好みか。
function delay(seconds) {
return new Promise((resolve, reject) => setTimeout(resolve, seconds));
}
async function cleanedFunc() {
try {
await delay(1500);
const result1 = 1;
console.log(initialData); // 1
await delay(1000);
const result2 = result1 + 1;
console.log(result2); // 2
await delay(500);
const result3 = result2 + 1;
console.log(result3); // 3
} catch (error) {
console.error(error);
}
}
cleanedFunc();
まとめ
以上、自分が良く出くわす非同期処理の良くない(と思っている)実装例となる。
非同期処理はバグってる実装(実行順が制御されていないとか)でも、テストデータや環境によっては一見うまく動いて見えたりして、テストだけではなかなかバグが気付けないことも多い。
やはりコーディングの段階でバグを減らすのが最も効率的なので、そのためにも可読性には注意したい。