Vue.jsのエラーハンドラについてのメモ

Vue.js (Vue3)では エラーハンドリングの仕組みとして
- [onErrorCaptured](https://ja.vuejs.org/api/composition-api-lifecycle#onerrorcaptured)
- [app.config.errorHandler](https://ja.vuejs.org/api/application#app-config-errorhandler)

が提供されています。

これらを使うことでコンポーネント内で発生したエラーのハンドリングを行うことができます。

## onErrorCaptured

各コンポーネントで定義をするComposition API用のエラーハンドラーです。

Options APIの場合は[errorcaptured](https://ja.vuejs.org/api/options-lifecycle#errorcaptured)を参照してください。

以下のように使います。

``` vue:ParentComponent.vue
<script setup lang="ts">
import { onErrorCaptured } from "vue";

onErrorCaptured((error, insttance, info) => {
  // error: エラーのインスタンス
  // instance: ソースコンポーネント
  // info: なんかの情報

  // errorの情報をもとにハンドリング
  console.log("エラーが発生しました。")
});
</script>
```

### 【注意】ハンドリングできるのは子孫コンポーネントのエラー

ドキュメントにも

> 子孫コンポーネントから伝搬するエラーをキャプチャーしたときに呼び出されるフックを登録します。

とあるように ハンドリングできるのは子孫コンポーネントから伝播するエラーです。

つまり以下のようなコードの場合、ボタンをクリックしてthrowErrorからスローされるエラーはハンドリングされません。

``` vue:ParentComponent.vue
<script setup lang="ts">
import { onErrorCaptured } from "vue";

onErrorCaptured((error, insttance, info) => {
  // error: エラーのインスタンス
  // instance: ソースコンポーネント
  // info: なんかの情報

  // errorの情報をもとにハンドリング
  console.log("エラーが発生しました。")
});

function throwError() {
  // ここでスローしても上のonErrorCapturedには入りません！
  throw new Error("エラーだ！")
}
</script>

<template>
  <div>
    <button @click="throwError">error</button>
  </div>
</template>
```

ハンドリングが行われるのは以下のような場合です。

子コンポーネント
``` vue:SubComponent.vue
<script setup lang="ts">
function throwError() {
  throw new Error("エラーだ！")
}
</script>

<template>
  <div>
    <button @click="throwError">error</button>
  </div>
</template>
```

親コンポーネント
``` vue:ParentComponent.vue
<script setup lang="ts">
import { onErrorCaptured } from "vue";
import SubComponent from "./SubComponent.vue";

onErrorCaptured((error, insttance, info) => {
  // error: エラーのインスタンス
  // instance: ソースコンポーネント
  // info: なんかの情報

  // errorの情報をもとにハンドリング
  console.log("エラーが発生しました。");
});
</script>

<template>
  <div>
    <SubComponent />
  </div>
</template>
```

上記の場合では `SubComponent` でエラーがスローされた場合は `ParentComponent` でハンドリングが行われます。


### 伝播を止めたい場合はfalseを返す

先のコードでは エラーハンドリングは行われるのですが、そのままエラーは親に伝播するため、別のエラーハンドラでも処理が行われます。

この伝播を止めたい場合は falseを返却することで止めることができます。

``` vue:ParentComponent.vue
<script setup lang="ts">
import { onErrorCaptured } from "vue";
import SubComponent from "./SubComponent.vue";

onErrorCaptured((error, insttance, info) => {
  // error: エラーのインスタンス
  // instance: ソースコンポーネント
  // info: なんかの情報

  // errorの情報をもとにハンドリング
  console.log("エラーが発生しました。");

  // falseを返すことで伝播を止める。
  return false
});
</script>
```

### 【注意】全てのエラーのハンドリングが行われるわけではない

ドキュメントにも書かれていますが、ハンドリングが行われるのはVue関連のイベントでエラーがスローされた場合です。

例えばですが以下のような、windowのイベントのコールバックや、setTimeoutのコールバックからエラーがスローされた場合はハンドリングが行われません。

``` vue:SubComponent2.vue
<script setup lang="ts">
import { onBeforeUnmount, onMounted } from "vue";

function throwError() {
  throw new Error("エラーだ！");
}

// ---------------------
// リサイズのハンドラからエラー

onMounted(() => {
  window.addEventListener("resize", throwError);
});
onBeforeUnmount(() => {
  window.removeEventListener("resize", throwError);
});

// ---------------------
// setTimeoutからエラー
function handleClick1() {
  setTimeout(() => {
    throwError();
  }, 1000);
}
</script>

<template>
  <div>
    <button @click="handleClick1">error1</button>
  </div>
</template>

```

setTimeoutでも以下のようにawait していた場合はハンドリングが行われます。

``` vue:SubComponent2.vue
<script setup lang="ts">
function throwError() {
  throw new Error("エラーだ！");
}

// ---------------------
// setTimeoutだがawait している場合
async function handleClick2() {
  await new Promise<void>((resolve) => setTimeout(() => resolve(), 1000));
  // このErrorは親でハンドリングされる。
  throwError();
}
</script>

<template>
  <div>
    <button @click="handleClick2">error2</button>
  </div>
</template>
```

なので 非同期処理を実行する場合はこれらの内容を意識しておく必要があります。


## app.config.errorHandler

アプリケーション内で発生したエラーをハンドリングします。

以下のようにして使います。

``` ts:main.ts

import { createApp } from "vue";

import App from "./App.vue";
import router from "./router";

const app = createApp(App);
app.use(router);

app.config.errorHandler = (error, instance, info) => {
  // error: エラーのインスタンス
  // instance: ソースコンポーネント
  // info: なんかの情報

  // errorの情報をもとにハンドリング
  console.log("エラーが発生しました2。");

  // (例)エラー画面に遷移
  router.push("/error");
};

app.mount("#app");
```

注意点としては `onErrorCaptured` と同様で全てのエラーがハンドリングされるわけではありません。

## 番外編

Vueのエラーハンドラが反応しないエラーについては windowの`error`, `unhandledrejection` をハンドリングすることで可能です。

``` ts:main.ts
const app = createApp(App);

// 省略

window.addEventListener("error", (event: ErrorEvent) => {
  console.log("エラーハンドラでエラーをキャッチ");
  console.log(event.error)
  
  // エラー画面に遷移
  router.push("/error");
})

window.addEventListener("unhandledrejection", (event: PromiseRejectionEvent) => {
  console.log("未ハンドリングのreject");
  console.log(event.reason)
  
  // エラー画面に遷移
  router.push("/error");
})

app.mount("#app");

```

逆にこれだけでもよい気がしますが `errorHandler` だとその他の情報もハンドラの引数から得ることができるので、そういった情報を使いたい場合は `errorHandler` を使うとよいかもしれないですね。

## まとめ

以上Vue.jsで提供されているエラーハンドリングの仕組みについての紹介でした。

番外編の内容と組み合わせることでよりよいエラーハンドリングができるようになると思います。