Vue.config.errorHandlerはどこで発生したエラーをキャプチャできるのか

どこで発生したエラーが`Vue.config.errorHandler`でキャプチャできて、どれができないのか、できないやつはどうすればいいのかについて。

# 🎉 Vue.js v2.6 Macross Released!

2019/03/03追記: [Vue.jsの2.6.0](https://medium.com/the-vue-point/vue-2-6-released-66aa6c8e785e)で、キャプチャ可能なエラーの発生シチュエーションが増えたためアップデートしました。

# 忙しい人向け

これで一通りキャプチャできるはず。

```js:main.js
Vue.config.errorHandler = (err, vm, info) => {
  console.log(`Captured in Vue.config.errorHandler: ${info}`, err);
};
window.addEventListener("error", event => {
  console.log("Captured in error EventListener", event.error);
});
window.addEventListener("unhandledrejection", event => {
  console.log("Captured in unhandledrejection EventListener", event.reason);
});
```

# 検証用URL

https://clomie.github.io/vue-error-capturing/
console表示させた状態でボタンぽちぽちしてください。

ソースコード: https://github.com/clomie/vue-error-capturing

# 対象バージョン
現時点での最新版。
Vue.js: v2.6.8
Nuxt.js: v2.0.0

# まとめ
`Vue.config.errorHandler`でキャプチャできるエラーは下記で発生したもののみ。

- 描画関数、またはテンプレート
  - フィルタ関数
  - 算出プロパティ関数
- ウォッチャ
- 各種インスタンスライフサイクルフック ([v2.2.0以降](https://github.com/vuejs/vue/commit/3c0cdb553590db84a3bdb2a299947e7424e23464))
- `data`関数 ([v2.2.5以降](https://github.com/vuejs/vue/commit/59a372229b014de8cd610973f9428da63e2d72cc))
- 各種ディレクティブフック ([v2.3.0以降](https://github.com/vuejs/vue/commit/3c0265517b55afe747948c193b79954d18b4d4c8))
- `nextTick`関数のコールバック ([v2.3.0以降](https://github.com/vuejs/vue/commit/4b6913cb46be2f8f932f19adb63bbc6f388a3b76))
- カスタムイベントにバインドしたハンドラ関数 ([v2.4.0以降](https://github.com/vuejs/vue/commit/11b7d5dff276caa54da3ef5b52444c0e2c5bbf41))
- DOMネイティブイベントにバインドしたハンドラ関数([v2.6.0以降](https://github.com/vuejs/vue/commit/6e9fcfc81d922a1b188268bf50d7e67c07d6d662))
- async修飾子付きの各種インスタンスライフサイクルフック([v2.6.0以降](https://github.com/vuejs/vue/commit/6e9fcfc81d922a1b188268bf50d7e67c07d6d662))
- async修飾子付きのイベントハンドラ関数([v2.6.0以降](https://github.com/vuejs/vue/commit/6e9fcfc81d922a1b188268bf50d7e67c07d6d662))
- 上記の関数から直接呼び出された関数

Nuxt.jsの場合、上記に加えて

- `fetch`、`asyncData`関数([Nuxt.js v2.0.0以降](https://github.com/nuxt/nuxt.js/releases/tag/v2.0.0))

下記で発生したエラーは`Vue.config.errorHandler`ではキャプチャできない。それぞれwindowオブジェクトに`error`, `unhandledrejection`のイベントハンドラを登録することでグローバルにキャプチャできる。

- setTimeoutやsetIntervalなどの非同期コールバック関数 (`error`)
- Promise経由で呼び出される処理 (`unhandledrejection`)
- イベントハンドラ、ライフサイクルフック以外のasync修飾子付きの関数 (`unhandledrejection`)

# Vue.config.errorHandler

[Vue.js > API > グローバル設定 > errorHandler](https://jp.vuejs.org/v2/api/#errorHandler)

実際には[src/core/util/error.js](https://github.com/vuejs/vue/blob/v2.6.8/src/core/util/error.js)で処理されています。
`handleError`、および`invokeWithErrorHandling`関数がexportされていて、使用箇所を追ってみると、それぞれどこで発生したエラーをキャプチャしているのかがわかります。

Search · handleError repo:vuejs/vue path:/src/core
https://github.com/search?q=handleError+repo%3Avuejs%2Fvue+path%3A%2Fsrc%2Fcore&type=Code

Search · invokeWithErrorHandling repo:vuejs/vue path:/src/core
https://github.com/search?q=invokeWithErrorHandling+repo%3Avuejs%2Fvue+path%3A%2Fsrc%2Fcore&type=Code

`invokeWithErrorHandling`が使われている箇所は、async関数などのPromiseを返す関数のエラーに対応しています。

注意点として、`Vue.config.errorHandler`でキャプチャ可能なエラーは、たとえ設定していなかったとしても、後述の`error`、または`unhandledrejection`イベントには伝播しません。
これは、Vue.js内のエラー処理で、ブラウザ上で動作している場合、console.errorに直接エラーを出力しているためです。
https://github.com/vuejs/vue/blob/v2.6.8/src/core/util/error.js#L78

# Vue.config.warnHandler

[Vue.js > API > グローバル設定 > warnHandler](https://jp.vuejs.org/v2/api/#warnHandler)

developmentモードでビルドしている際に、コンソールに出力される`[Vue warn]: `で始まるメッセージについては、このハンドラで受け取れます。ただし、**productionモードでビルドするとwarnメッセージは出力されない**ため、このハンドラも無視されます。

実際は[src/core/util/debug.js](https://github.com/vuejs/vue/blob/v2.6.8/src/core/util/debug.js)で処理されており、`warn`関数としてexportされています。使用箇所は下記のリンクから探せますが、こっちは`errorHandler`よりは多いです。

Search · warn repo:vuejs/vue path:/src/core
https://github.com/search?q=warn+repo%3Avuejs%2Fvue+path%3A%2Fsrc%2Fcore&type=Code

よく遭遇するwarnメッセージとしては下記のようなものが挙げられるかと思います。これらは`errorHandler`ではキャプチャできません。

- template内で使っているコンポーネントのcomponents登録忘れ、コンポーネント名ミス
- template内のプロパティ名ミス
- propsのバリデーションエラー

# error イベント

GlobalEventHandlers.onerror - Web APIs | MDN
https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror

JavaScriptコード内で発生したキャッチされていないエラーをキャプチャできます。
前述の通り、`Vue.config.errorHandler`でキャプチャ可能なエラーは`error`イベントには伝播しません。

コンポーネントに定義したメソッドであっても、setTimeoutやsetIntervalといったネイティブな非同期コールバックからの呼び出し等の場合、Vue.jsのコアライブラリが間に挟まる余地が無いため、`errorHandler`ではキャプチャできません。

# unhandledrejection イベント

unhandledrejection - Event reference | MDN
https://developer.mozilla.org/en-US/docs/Web/Events/unhandledrejection

Promiseの処理でcatchされていないRejectをキャプチャできます。ただし、`Vue.config.errorHandler`でキャプチャ可能なエラーはキャプチャできません。また、[対応しているブラウザが限られている](https://caniuse.com/#search=unhandledrejection)点に注意が必要です。
Promise内で発生したエラーについても同様にVue.jsのコアライブラリが間に挟まる余地がないため、`errorHandler`ではキャプチャできません。

# vm.errorCaptured フック

[Vue.js > API > オプション / ライフサイクルフック > errorCaptured](https://jp.vuejs.org/v2/api/#errorCaptured)

`errorHandler`と同じ用途で、コンポーネントに定義するライフサイクルフックとして`errorCaptured`があります。
**子孫コンポーネントで発生したエラー**に対して反応する点に注意が必要です。設定したコンポーネント自体で発生したエラーには反応しません。

# その他の参考リンク

[かずぽんブログ • Vue\.js 2017年まとめ & 今後](http://blog.kazupon.jp/post/168909661613/2017-vuejs-in-review)
[Vue\.jsでフロント側のエラー検知を共通化する \- エス・エム・エスキャリア開発者ブログ](http://sms-c-engineer.hatenablog.com/entry/2018/04/24/142445)
[Vue\.jsのerrorHandlerがいまいちつかめない \- Qiita](https://qiita.com/kai_kou/items/90255bdcfaa44f5d1de9)
[ユーザのブラウザで起きた JavaScript のエラーを収集する \- Qiita](https://qiita.com/grapswiz/items/71f14e73820e352c1fa3)