Nuxt3入門(第6回) - アプリケーションで発生するエラーに対応する
前回はNuxt3の設定情報管理について見てきました。
今回のテーマは実用的なアプリケーションに不可欠なエラーハンドリングです。
Nuxtはクライアントサイドだけでなく、サーバーサイドレンダリングもサポートするハイブリッドフレームワークです。
このため、エラー発生箇所に対応した適切なハンドリングが求められます。
Vueコンポーネントのエラー
#Vueコンポーネントのレンダリングやライフサイクルメソッド、setup等、エラーが発生する場所は多くあります。
機能固有のエラーハンドリングはtry/catchやPromiseを用いることになりますが、ここではVue/Nuxtがフレームワークとして提供している仕組みについて見ていきます。
onErrorCaptured
#Nuxtではありませんが、VueではonErrorCaptured イベントフックを提供しています。
これはサブコンポーネントで未捕捉のエラーが発生した時に呼び出されるフックです[1]。
例としてサブコンポーネントのmountedでエラーが発生したとします。
ページコンポーネントのソースコードは以下のようになります。
<script setup lang="ts">
const message = ref('');
onErrorCaptured((err) => {
console.log('onErrorCaptured', err);
message.value = err.message;
})
</script>
<template>
<div>
<p>{{ message }}</p>
<FlakyComponent />
</div>
</template>
エラーが発生するサブコンポーネント(FlakyComponent)は以下です。
<script setup lang="ts">
onMounted(() => {
throw createError('FlakyComponentでエラーが発生しました!');
})
</script>
<template>
<div>サブコンポーネント</div>
</template>
サブコンポーネントの方で使用しているcreateErrorは、Nuxt3が提供するユーティリティです。
これを実行するとサブコンポーネントのmountedでエラーが発生します。
このエラーはonErrorCapturedフックで捕捉され、エラーメッセージが表示されるようになります。
なお、onErrorCapturedに指定したコールバック関数は何も返却していませんので、上位のイベントフックがある場合はそれらも実行されます。
コールバック関数でfalseを返却すると、このエラーハンドリング伝播を止めることができます。
ここでエラーを発生させたmountedはクライアントサイドでのみ実行されるVueのライフサイクルイベントです。
サーバーサイドでも実行されるsetupでエラーが発生するとどうなるでしょうか?
サブコンポーネント(FlakyComponent)は以下のようになります。
<script setup lang="ts">
throw createError('FlakyComponentでエラーが発生しました!');
</script>
<template>
<div>サブコンポーネント</div>
</template>
これを実行すると以下のようになります。
フォールバック用のメッセージ表示ではなく、エラーページに遷移してしまいました。
ここでもサーバーサイドでonErrorCapturedのコールバック関数は実行されます。しかし、Nuxtはサーバーサイドレンダリングでエラーが発生するとクリティカルエラーと判断し、専用のエラーページを返す仕様となっています(500エラー)[2]。
このようにサーバーサイドの実行で未捕捉のエラーが発生した場合は、onErrorCapturedを使っても意図しない結果を招く場合がありますので注意が必要です[3]。
onErrorCapturedは、配下のサブコンポーネントで発生した未捕捉のエラー発生時に呼び出されます。
自コンポーネントのエラーは対象外ということもあり、使い所が難しいフックと言えるかもしれません。
とはいえ、エラーレポートシステム等を使う等、グローバルに未捕捉のエラーを検知したいことは多いかと思います。
このケースではVueが提供するerrorHandlerを利用します。
plugins
ディレクトリに以下のようなプラグインを作成すれば、未捕捉のエラーが発生した場合に指定した関数が実行されます。
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.config.errorHandler = (err, context) => {
console.log("vue based error handler", err, context)
}
})
ただし、ここで捕捉できるエラーはVueに関するもののみで、全てのエラーを検知できる訳ではありません。
例えば、setTimeout/setIntervalのコールバック関数内でのエラーは検知しません。
全てのエラーを検知したい場合は、別途Windowのerrorイベント等のフックと併用する必要があります。
Nuxtアプリケーションの起動時に発生するクリティカルなエラー検知は、app:error
イベントフックを使用します。こちらの詳細はNuxtの公式ドキュメントを参照してください。
NuxtErrorBoundaryコンポーネント
#クライアントサイド限定ですが、NuxtErrorBoundaryを使うと、簡単にエラー発生時の影響を局所化できます。
ソースコードを見ればすぐに分かりますが、NuxtErrorBoundaryは前述のonErrorCapturedフックを使って配下のコンポーネントで発生するエラーを監視するNuxtのユーティリティコンポーネントです。
<script>
const log = (err) => console.log(err)
</script>
<template>
<div>
<NuxtErrorBoundary @error="log">
<!-- default slot -->
<FlakyComponent />
<!-- フォールバック -->
<template #error="{ error }">
エラーが発生しました。
{{ error }}
</template>
</NuxtErrorBoundary>
</div>
</template>
NuxtErrorBoundaryはデフォルトスロットに対象コンポーネント、名前付きスロット(error)にエラー発生時のフォールバックコンテンツを指定します[4]。
注意点として、ハイドレーション中はフォールバックが実行されないよう制御されています。このため対象コンポーネントのmounted等で発生したエラーには反応しません。
また、フォールバック時は上位のエラーハンドラへの伝播も行われませんので、グローバルなエラーハンドラでは検知されません。
APIアクセスエラー
#Nuxt3ではAPIアクセス時にuseFetch/useAsyncData Composableを使うことが多いかと思います。
この点のエラーハンドリングについても見てみます。
useFetch/useAsyncData自体は例外をスローしませんので、try-await/catch等でハンドリングしてもcatch節は実行されません。
これらは戻り値としてerror
を返却しますので、それを受け取る必要があります。
テンプレートでエラーハンドリングをする例は、以下のようになります。
<script setup lang="ts">
const { data: articles, error } = await useFetch('/api/blogs');
// useAsyncDataを使う場合
// const { data: articles, error } = await useAsyncData(() => $fetch('/api/blogs'));
</script>
<template>
<div>サブコンポーネント</div>
<div v-if="articles">
<p>成功</p>
{{ articles }}
</div>
<div v-else-if="error">
<p>エラーが発生しました</p>
</div>
</template>
なお、error
はサーバーサイドではエラー詳細が格納されていますが、クライアントサイドのハイドレーション実行後はboolean型(エラー時はtrue)になります。
これは、不用意にエラー内容をクライアントサイドに公開しないためのNuxtのセキュリティ面での配慮です。
Errorの内容(ステータスコード等)をクライアントサイドで保持したい場合は、別途状態を保持するように実装する必要があります[5]。
カスタムエラーページの作成
#Nuxtは、サーバーサイド実行でエラーが発生した場合や、クライアントサイドでクリティカルなエラーが発生した場合に専用のエラーページを表示します。
もちろんこのエラーページはカスタマイズ可能です。
カスタムエラーページを作成する場合は、プロジェクトルート直下にerror.vue
を作成するだけです。
以下のようなものになります。
<script setup lang="ts">
import { NuxtApp } from "#app";
const props = defineProps<{ error: NuxtApp["payload"]["error"] }>();
const handleError = () => clearError({redirect: '/'})
const isDev = process.dev;
</script>
<template>
<p>エラーが発生しました</p>
<button @click="handleError">トップページに戻る</button>
<div v-if="isDev">
{{ error }}
</div>
</template>
エラーページも通常のVueコンポーネントです。
propsとしてエラー発生内容が格納されているerror
を受け取れます。ここでは開発モード(npm run dev
)の場合のみエラー内容の詳細を表示するようにしています。
上記では、エラーページで「トップページに戻る」ボタンを配置し、そのイベントハンドラでNuxtユーティリティのclearErrorを呼んでいます。
この関数はNuxtアプリケーション(NuxtApp.payload.error)が内部で保持しているエラーをクリアするものです。
引数にリダイレクト先(ここではトップページ)を指定することで、クリア後に通常のページへ復帰できるようになります。
クライアントサイドで例外をスローするとデフォルトは非クリティカルなエラーになり、エラーページは表示されません。
明示的にエラーページを表示する場合は、ユーティリティ関数として用意されているshowErrorを使用します。
または、例外スロー時に使用するcreateErrorの引数で、fatalをtrueに指定するとエラーページを表示できます[6]。
// プログラムでエラーページ表示
const moveError = () => {
showError('FlakyComponentでエラーが発生しました!')
}
// エラー生成時にfatal:trueを指定
onMounted(() => {
throw createError({ message: 'FlakyComponentでエラーが発生しました!', fatal: true });
})
まとめ
#ここでは、Nuxt3が提供するエラーハンドリングを見てきました。
Nuxtだけでなく、Vueで用意されているものも含めて、うまく使って堅牢でデバッグしやすいアプリケーションとしたいものです。
次回はプラグイン/ミドルウェア開発について見ていく予定です。