NuxtErrorBoundary でエラー発生時の UI を簡単に実装する
この記事は、ユアマイスター アドベントカレンダー 2022の 13 日目の記事です。
こんにちは。
ユアマイスターでエンジニアをしている南です。
この記事では、 Nuxt3 で新しく導入された NuxtErrorBoundary を使って、クライアント側のエラーが発生した時の UI を簡単に実装する方法を紹介します。
NuxtErrorBoundary とは
子コンポーネントで発生したクライアント側のエラーの処理を実装できるコンポーネントです。
例えば以下のようなケースの場合、詳細なエラーを見るためには、以下のようにコンソールを開いて確認する必要があります。
ボタンをクリックしてデータを読み込む時に失敗し、適切にエラーハンドリングがされていないようなケース。
NuxtErrorBoundary を使用するメリット
メリットは、子コンポーネントで発生したエラーの処理を簡単に実装できる点です。
NuxtErrorBoundary を使わなくても、子コンポーネントでエラーによって出し分け処理を行うことで同様のことが実現できます。
ただその場合、子コンポーネント側でエラーによって出し分けの処理を書く必要が出てきます。
NuxtErrorBoundary の場合は、子コンポーネント側でエラーの出し分け処理を書かなくても、NuxtErrorBoundary で囲むだけで対応することができます。
NuxtErrorBoundary の使い方
対象の子コンポーネントを NuxtErrorBoundary コンポーネントで囲むようにします。
pages/index.vue
の中身
<script setup lang="ts">
import { useErrorBoundary } from '~/compsables/error-boundary'
import FetchArticleButton from '~/components/FetchArticleButton.vue'
const { clearError } = useErrorBoundary()
</script>
<template>
<div class="flex place-content-center items-center">
<div class="m-8 w-full max-w-3xl bg-white p-2">
<NuxtErrorBoundary>
<FetchArticleButton />
<template #error="{ error }">
<p>記事取得時にエラーがおきました。</p>
<p>前の画面に戻って再度お試しください。</p>
<button class="bg-green-400 p-2 text-white" @click="clearError(error)">前の画面に戻る</button>
</template>
</NuxtErrorBoundary>
</div>
</div>
</template>
上記のコンポーネントは、エラーを捕捉した時に以下のように表示されます。
components/FetchArticleButton.vue
の中身
<script setup lang="ts">
import { useArticle } from '~/compsables/article'
const { fetch } = useArticle()
</script>
<template>
<button @click="fetch">記事取得</button>
</template>
composables/article.ts
の中身
import { createError } from '#app'
export const useArticle = () => {
const fetch = () => {
// ... 記事取得処理が本来入ります。
throw createError({
statusCode: 400,
message: '記事取得に失敗しました。',
})
}
return { fetch }
}
composables/error-boundary.ts
の中身
import { Ref } from 'vue'
export const useErrorBoundary = () => {
const clearError = (error: Ref<Error | null>) => {
error.value = null
}
return { clearError }
}
また NuxtErrorBoundary を使ってエラーを処理する方法は、以下の公式ドキュメントにあるように 2 つあります。
<NuxtErrorBoundary> · Nuxt Components
- イベントから渡ってくるエラーを受け取る方法
- エラー捕捉時に代替となるコンテンツを表示する方法
1. イベントから渡ってくるエラーを受け取る方法
@error
の箇所にエラーイベントが発生した時に実行する関数を指定します。
またエラーを実行する関数の第一引数を指定することで、エラーオブジェクトを受け取ることができます。
<script setup lang="ts">
import FetchArticleButton from '~/components/FetchArticleButton.vue'
import { useLog } from '~/compsables/log'
const { log } = useLog()
</script>
<template>
<div class="flex place-content-center items-center">
<div class="m-8 w-full max-w-3xl bg-white">
<NuxtErrorBoundary @error="log">
<FetchArticleButton />
</NuxtErrorBoundary>
</div>
</div>
</template>
composables/log.ts
の中身
export const useLog = () => {
const log = (error: Error) => {
console.log(error)
}
return { log }
}
2. エラー捕捉時に代替となるコンテンツを表示する方法
以下のようにerror
スロットを指定することで、エラーを捕捉した時に表示するコンテンツを指定することができます。
<script setup lang="ts">
import FetchArticleButton from '~/components/FetchArticleButton.vue'
</script>
<template>
<div class="flex place-content-center items-center">
<div class="m-8 w-full max-w-3xl bg-white">
<NuxtErrorBoundary>
<FetchArticleButton />
<template #error="{ error }">
<p>エラーが起きました。 {{ error }}</p>
</template>
</NuxtErrorBoundary>
</div>
</div>
</template>
NuxtErrorBoundary で表示されている UI の切り替え方法
ref
でラップされているエラーオブジェクトにnull
をセットすることで、表示されているコンテンツを切り替えることができます。
切り替えた場合は、エラーを捕捉する前のコンテンツが表示されます。
<script setup lang="ts">
import { useErrorBoundary } from '~/compsables/error-boundary'
import FetchArticleButton from '~/components/FetchArticleButton.vue'
const { clearError } = useErrorBoundary()
</script>
<template>
<div class="flex place-content-center items-center">
<div class="m-8 w-full max-w-3xl bg-white p-2">
<NuxtErrorBoundary>
<FetchArticleButton />
<template #error="{ error }">
<p>記事取得時にエラーがおきました。</p>
<p>前の画面に戻って再度お試しください。</p>
<button class="bg-green-400 p-2 text-white" @click="clearError(error)">前の画面に戻る</button>
</template>
</NuxtErrorBoundary>
</div>
</div>
</template>
clearError
の箇所
import { Ref } from 'vue'
export const useErrorBoundary = () => {
const clearError = (error: Ref<Error | null>) => {
error.value = null
}
return { clearError }
}
NuxtErrorBoundary で捕捉されないパターン
元のソースコードを見てみるといくつかのパターンの場合は、捕捉されません。
framework/nuxt-error-boundary.ts at main · nuxt/framework
捕捉されないパターンとしては、以下の通りです。
- Hydration 時
- サーバーサイドでのエラー時
- NuxtErrorBoundary と同じコンポーネント内でエラーが発生した時
Hydration 時
Hydration とは、以下の一連のプロセスのことです。
- サーバーサイドや静的ホスティングからレンダリングされた静的な HTML に対して、UI フレームワーク側が持つ仮想 DOM を使って比較
- 差分がなければ UI フレームワーク側が、イベントリスナーを登録し静的な HTML を動的な DOM に変更
Hydration 中かどうかは、isHydrating
というプロパティで判定できます。
実際の動作は、以下のコンポーネントを準備すると挙動を確認できます。
components/SubComponent.vue
を以下のように作成します。
<script setup lang="ts">
import { onMounted } from '#imports'
import { createError, useNuxtApp } from '#app'
const nuxtApp = useNuxtApp()
console.debug('isHydrating:', nuxtApp.isHydrating)
onMounted(() => {
throw createError({
statusCode: 400,
message: 'onMountedでのエラーです。',
})
})
</script>
<template>
<p>サブコンポーネントです。</p>
</template>
pages
ディレクトリに任意のディレクトリを追加します。
任意のディレクトリを追加した後index.vue
を以下のようにします。
<script setup lang="ts">
import SubComponent from '~/components/SubComponent'
</script>
<template>
<div class="flex place-content-center items-center">
<div class="m-8 w-full max-w-3xl bg-white">
<NuxtErrorBoundary>
<SubComponent />
<template #error="{ error }">
<p>An error occurred: {{ error }}</p>
</template>
</NuxtErrorBoundary>
</div>
</div>
</template>
任意のディレクトリを立ち上げた後npm run build
を実行します。
その後node .output/server/index.mjs
コマンドでローカルのサーバーを立ち上げます。
立ち上げたら作成したディレクトリにアクセスします。
初回アクセス時は、サーバー側にリクエストが飛んでクライアント側で Hydration が実行されます。
そのためSubComponent.vue
内のクライアント側で動くonMounted
内部でのエラーが捕捉されず、コンソールにエラーが表示されます。
次に一度別のページにアクセスした後、NuxtLink 経由で先程作成した任意のディレクトリにアクセスします。
すると今度はエラーが捕捉されます。
この時のisHydrating
は、false
になっており Hydration 中ではないということが示されています。
サーバーサイドでのエラー時
NuxtErrorBoundary のソース上では、以下のようにprocess.client
でクライアント側かどうかを見ています。
onErrorCaptured((err) => {
if (process.client && !nuxtApp.isHydrating) {
emit('error', err)
error.value = err
return false
}
})
framework/nuxt-error-boundary.ts at main · nuxt/framework
そのためサーバーサイドでエラーが発生すると捕捉されず、 Nuxt 側でエラーページへ遷移されるようになります。
この挙動を確認するには、SubComponent.vue
を以下のように書き換えます。
<script setup lang="ts">
import { createError } from '#app'
throw createError({
statusCode: 500,
message: 'サーバーサイドのエラーです。',
})
</script>
<template>
<p>サブコンポーネントです。</p>
</template>
またサーバーサイドで実行させるためには、 SSR モードで実行する必要があります。
そのためnuxt.config.ts
のssr
の項目をtrue
に変更します。
この状態でアクセスを行うと以下のように表示されます。
NuxtErrorBoundary と同じコンポーネント内でエラーが発生した時
NuxtErrorBoundary は、同じコンポーネント内で発生したエラーを捕捉することができません。
これは、元のソースコードを見るとわかりますが、NuxtErrorBoundary は、 Vue3 のonErrorCaptured
を使用しているためです。
Composition API: Lifecycle Hooks | Vue.js
動作を確認する場合は、コンポーネントを以下のようにします。
<script setup lang="ts">
import { useArticle } from '~/compsables/action'
import { useErrorBoundary } from '~/compsables/error-boundary'
const { clearError } = useErrorBoundary()
const { fetch } = useArticle()
</script>
<template>
<div class="flex place-content-center items-center">
<div class="m-8 w-full max-w-3xl bg-white p-2">
<NuxtErrorBoundary>
<button @click="fetch">記事取得</button>
<template #error="{ error }">
<p>記事取得時にエラーがおきました。</p>
<p>前の画面に戻って再度お試しください。</p>
<button class="bg-green-400 p-2 text-white" @click="clearError(error)">前の画面に戻る</button>
</template>
</NuxtErrorBoundary>
</div>
</div>
</template>
同じコンポーネント内で発生したエラーの場合は、上記ボタンをクリックしてもエラーは捕捉されません。