yourmystar tech blog
著者: 南 公開日:

NuxtErrorBoundary でエラー発生時の UI を簡単に実装する

この記事は、ユアマイスター アドベントカレンダー 2022の 13 日目の記事です。

こんにちは。

ユアマイスターでエンジニアをしている南です。

この記事では、 Nuxt3 で新しく導入された NuxtErrorBoundary を使って、クライアント側のエラーが発生した時の UI を簡単に実装する方法を紹介します。

NuxtErrorBoundary とは

子コンポーネントで発生したクライアント側のエラーの処理を実装できるコンポーネントです。

例えば以下のようなケースの場合、詳細なエラーを見るためには、以下のようにコンソールを開いて確認する必要があります。

ボタンをクリックしてデータを読み込む時に失敗し、適切にエラーハンドリングがされていないようなケース。

Prose Components

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>

上記のコンポーネントは、エラーを捕捉した時に以下のように表示されます。

Prose Components

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. イベントから渡ってくるエラーを受け取る方法
  2. エラー捕捉時に代替となるコンテンツを表示する方法

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 とは、以下の一連のプロセスのことです。

  1. サーバーサイドや静的ホスティングからレンダリングされた静的な HTML に対して、UI フレームワーク側が持つ仮想 DOM を使って比較
  2. 差分がなければ 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内部でのエラーが捕捉されず、コンソールにエラーが表示されます。

Prose Components

次に一度別のページにアクセスした後、NuxtLink 経由で先程作成した任意のディレクトリにアクセスします。

すると今度はエラーが捕捉されます。

Prose Components

この時の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.tsssrの項目をtrueに変更します。

この状態でアクセスを行うと以下のように表示されます。

Prose Components

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>

同じコンポーネント内で発生したエラーの場合は、上記ボタンをクリックしてもエラーは捕捉されません。

Prose Components

ポストするはてなブックマークに追加シェアする