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

# 目的
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. 非同期の結果を使う場合
- 2. エラーが発生する場合
- 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); // "デフォルト値"
})()
```