目的
JavaScriptにおけるasync/awaitとPromiseは同じものですが、
エラーハンドリングに関しては違いがあったり、returnに気をつけないといけなかったり、
はまったり、見落としたりする箇所があるので、まとめておきます。
async/awaitとPromiseの関係性については、記事中で詳しくは触れません。
おさらい
Promise
正常系
// 非同期でメッセージがかえってくる
const pResult = Promise.resolve("結果です");
// 結果が帰ってきてないので、Promise型
console.log(pResult.toString()); // "[object Promise]"
// thenで結果を待つ
pResult.then(result => {
console.log(result.toString()); // "結果です"
});
エラー発生
// 非同期でErrorがthrowされた状態
// RESTでAPIエラー起こすなどでここに入る
const pError = Promise.reject(new Error("エラーです"));
// 結果が帰ってきてないので、Promise型
console.log(pError.toString()); // "[object Promise]"
// catchでエラーを取る
pError
.then(result => {
console.log("到達しない!");
})
.catch(error => {
console.log(error.toString()); // "Error: エラーです"
});
async/await
正常系
// async(Promise)内でしかawaitできない
(async () => {
// 非同期でメッセージがかえってくる
const pResult = Promise.resolve("結果です");
// 結果が帰ってきてないので、Promise型
console.log(pResult.toString()); // "[object Promise]"
// awaitで結果を待つ
const result = await pResult;
console.log(result.toString()); // "結果です"
})()
エラー発生
(async () => {
// 非同期でErrorがthrowされた状態
// RESTでAPIエラー起こすなどでここに入る
const pError = Promise.reject(new Error("エラーです"));
// 結果が帰ってきてないので、Promise型
console.log(pError.toString()); // "[object Promise]"
// throwされるので、tryでエラーを取る
try {
await pError;
console.log("到達しない!");
} catch (error) {
console.log(error.toString()); // "Error: エラーです"
}
})()
returnとエラーハンドリング
どのようにreturnするかは、影響が大きいので、きちんと考えて書きます。
結果としてreturn書かなくても十分な場面は多いですが、returnを考慮した上で、書く、書かないを考えていきたい場所です。
returnの考慮が必須になる場面が3パターンほどあります、注意していきましょう。
-
- 非同期の結果を使う場合
-
- エラーが発生する場合
-
- 複数の非同期がある場合
1. 非同期の結果を使う場合
returnの有無でどうなるか、基本的な挙動なのでとりあえず覚えておきましょう。
returnを省略すると、undefinedになる
// Promise
const pResult = Promise.resolve("結果です");
pResult
.then(result => {
console.log(result) // "結果です"
// returnしない!
})
.then(result => {
// 結果が引き継がれない!
console.log(result) // undefined
})
async/awaitの場合、returnしなかったらundefinedになるのは直感的ですね。
// async/await
const pResult = Promise.resolve("結果です");
(async () => {
async function edit1(){
const result = await pResult;
console.log(result) // "結果です"
// returnしない!
}
const edited1 = await edit1();
console.log(edited1) // undefined
})()
returnは、後続の引数になる
// Promise
const pResult = Promise.resolve("結果です");
pResult
.then(result => {
console.log(result) // "結果です"
return `${result} 2回目`;
})
.then(result => {
console.log(result) // "結果です 2回目"
})
// async/await
const pResult = Promise.resolve("結果です");
(async () => {
async function edit1(){
const result = await pResult;
console.log(result) // "結果です"
return `${result} 2回目`;
}
const edited1 = await edit1();
console.log(edited1) // "結果です 2回目"
})()
2. エラーが発生する場合
正常系とほぼ同じ挙動ですが、
1つの非同期に対して、2パターン(正常、エラー)は常に考えていくので、考慮するパターンが純粋に増えていきます。
考慮するパターンが多いと、バグる確率がどんどん上がってきます。
catchでreturnを省略すると、undefinedで復旧する
catch後は正常系に戻りますが、returnを省略すると、後続の引数はundefinedになります。
なにかエラー処理をはさんだ時に、意図しない復旧をしてしまう場合があるので、注意です。
// Promise
const pError = Promise.reject(new Error("エラーです"));
pError
.catch(error => {
console.log(error.toString()); // "Error: エラーです"
// returnなし
})
.then(result => {
console.log(result); // undefined
})
こちらもasync/awaitであれば、直感的だと思います。
しかし、個人的にtry~catchは、Promiseでのcatchよりも視認性が悪いので好きではないです。
// async/await
const pError = Promise.reject(new Error("エラーです"));
(async () => {
async function edit1(){
try {
return await pError;
} catch(error){
console.log(error.toString()); // "Error: エラーです"
// returnなし
}
}
const edited1 = await edit1();
console.log(edited1); // undefined
})()
catchで書いたreturnは、復旧時の引数になる
エラー後、正常系に戻しつつ、デフォルトの挙動を指定したい場合などに書きます。
// Promise
const pError = Promise.reject(new Error("エラーです"));
pError
.catch(error => {
console.log(error.toString()); // "Error: エラーです"
return "デフォルト値";
})
.then(result => {
console.log(result); // "デフォルト値"
});
// async/await
const pError = Promise.reject(new Error("エラーです"));
(async () => {
async function edit1(){
try {
return await pError;
} catch(error){
console.log(error.toString()); // "Error: エラーです"
return "デフォルト値";
}
}
const edited1 = await edit1();
console.log(edited1); // "デフォルト値"
})()
復習ですが、awaitをreturnし忘れると、正常系がundefinedになるので、バグです。
try {
// これでは後続がundefined
// await pError;
// return必要
return await pError;
} ...
catch時、再throwでエラー継続
エラーハンドリングをした上で、後続の正常系を止めたい場合などは、throwします。
// Promise
const pError = Promise.reject(new Error("エラーです"));
pError
.catch(error => {
console.log(error.toString()); // "Error: エラーです"
throw error;
})
.then(result => {
console.log("到達しない!");
})
.catch(error => {
console.log(`${error.toString()} 2回目`); // "Error: エラーです 2回目"
})
// async/await
const pError = Promise.reject(new Error("エラーです"));
(async () => {
async function edit1(){
try {
return await pError;
} catch(error){
console.log(error.toString()); // "Error: エラーです"
throw error;
}
}
try {
const edited1 = await edit1();
console.log("到達しない!");
} catch(error){
console.log(`${error.toString()} 2回目`); // "Error: エラーです 2回目"
}
})()
3. 複数の非同期がある場合
1つの非同期で、2つ(正常、エラー)考えることがありましたが、
複数の非同期がある場合、全てで正常とエラー考慮が必要になってくるので、かなりつらい気持ちになると思います。
しかし残念ながら、複数の非同期をwaitしながら実行したり、エラーになったら後続を止めたかったりするのは、頻出です。
2回非同期をして、結果を使う
結果をわたすときはreturnします、return漏れるとundefinedです。
実はthenはflatMapになっているので、Promiseをreturnしても、result2でPromiseはネストになりません。
// Promise
const pResult1 = Promise.resolve("結果 1回目");
pResult1
.then(result1 => {
// 1回目の結果を使って、2回目の非同期をする
return Promise.resolve(`${result1} 2回目`);
})
.then(result2 => {
// result2はPromiseではない
console.log(result2); // "結果 1回目 2回目"
})
// async/await
const pResult1 = Promise.resolve("結果 1回目");
(async () => {
async function edit1(){
const result1 = await pResult1;
return Promise.resolve(`${result1} 2回目`);
}
const result2 = await edit1();
console.log(result2); // "結果 1回目 2回目"
})()
1つ目の非同期でエラーが起きたら、2つ目の非同期は実行しないが、正常系に戻す
returnさえ忘れなければ、おかしな動作はないです。
もし2つ目のPromiseをreturnし忘れ、エラーが発生した場合、catchに入らなくなるので、結構ハマります。
// Promise
const pResult1 = Promise.reject(new Error("エラーです"));
pResult1
.then(result1 => {
return Promise.resolve(`${result1} 2回目`);
})
.catch(error => {
return "デフォルト値";
})
.then(result2 => {
console.log(result2); // "デフォルト値"
})
// async/await
const pResult1 = Promise.reject(new Error("エラーです"));
(async () => {
async function edit1(){
const result1 = await pResult1;
return Promise.resolve(`${result1} 2回目`);
}
let result2;
try {
result2 = await edit1();
} catch(error){
result2 = "デフォルト値";
}
console.log(result2); // "デフォルト値"
})()
余談ですが、async/awaitだからといって、try~catch文を使わないといけないわけでは無いです。
Promiseのcatchを混ぜた方が綺麗です。
// async/await + Promise#catch
const pResult1 = Promise.reject(new Error("エラーです"));
(async () => {
async function edit1(){
const result1 = await pResult1;
return Promise.resolve(`${result1} 2回目`);
}
const result2 = await edit1().catch(error => "デフォルト値");
console.log(result2); // "デフォルト値"
})()