7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

async/await(Promise)のreturnとエラーハンドリング

Posted at

目的

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. 非同期の結果を使う場合
    1. エラーが発生する場合
    1. 複数の非同期がある場合

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); // "デフォルト値"
})()
7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?