本文 AI 產出,尚未審核

Vue3 課程 – 錯誤處理與除錯

主題:template 中的錯誤顯示策略


簡介

在 Vue3 應用程式中,template 是開發者與使用者互動的第一層介面。當資料或狀態異常時,若沒有適當的錯誤顯示策略,使用者很容易看到「空白」或「未預期」的畫面,進而失去信任感。

本篇文章將說明 在 Vue3 的 template 中,如何以結構化、可維護且使用者友善的方式呈現錯誤訊息。從基本的條件渲染、全域錯誤捕捉,到自訂錯誤組件與 TypeScript 型別防護,我們會一步步帶你建立完整的錯誤顯示機制,讓你的介面在任何例外情況下仍能保持可用性與一致性。


核心概念

1️⃣ 基本的條件渲染 – v-if / v-show

最直接的錯誤呈現方式是使用 v-ifv-show 依據錯誤狀態切換 UI。這種方式適合 單一欄位或局部錯誤,例如表單驗證失敗。

<template>
  <form @submit.prevent="onSubmit">
    <input v-model="email" type="email" placeholder="請輸入 Email" />
    <!-- 只在 emailError 為真時顯示錯誤訊息 -->
    <p v-if="emailError" class="error-msg">{{ emailError }}</p>

    <button type="submit">送出</button>
  </form>
</template>

<script setup>
import { ref } from 'vue'

const email = ref('')
const emailError = ref('')

// 簡易驗證範例
function onSubmit() {
  if (!email.value.includes('@')) {
    emailError.value = 'Email 必須包含 @ 符號'
  } else {
    emailError.value = ''
    // 送出資料...
  }
}
</script>

<style scoped>
.error-msg { color: red; margin-top: 4px; }
</style>

重點v-if 會在條件為 false 時完全移除元素,適合不需要保留 DOM 結構的情況;v-show 則僅切換 display,適合頻繁切換的場景。


2️⃣ 錯誤收集與集中管理 – 使用 ref/reactive 物件

在較大的表單或多個 API 請求時,單一變數的錯誤訊息會變得難以維護。此時可將錯誤資訊集中在一個 錯誤物件 中,並搭配 Object.keys 產生動態錯誤列表。

<template>
  <form @submit.prevent="handleSubmit">
    <div v-for="field in fields" :key="field.name" class="field-group">
      <label :for="field.name">{{ field.label }}</label>
      <input
        :id="field.name"
        v-model="form[field.name]"
        :type="field.type"
      />
      <!-- 依據 errors[field.name] 是否有值顯示錯誤 -->
      <p v-if="errors[field.name]" class="error-msg">
        {{ errors[field.name] }}
      </p>
    </div>

    <button type="submit">送出</button>
  </form>
</template>

<script setup>
import { reactive, ref } from 'vue'

const fields = [
  { name: 'username', label: '使用者名稱', type: 'text' },
  { name: 'password', label: '密碼', type: 'password' },
  { name: 'email',    label: 'Email',    type: 'email' }
]

const form = reactive({
  username: '',
  password: '',
  email: '',
})

const errors = reactive({
  username: '',
  password: '',
  email: '',
})

function handleSubmit() {
  // 先清空錯誤
  Object.keys(errors).forEach(k => (errors[k] = ''))

  // 基本驗證(實務上可改成 Yup、Vuelidate 等套件)
  if (!form.username) errors.username = '請輸入使用者名稱'
  if (form.password.length < 6) errors.password = '密碼長度至少 6 位'
  if (!form.email.includes('@')) errors.email = 'Email 格式不正確'

  // 若無錯誤則送出
  if (!Object.values(errors).some(msg => msg)) {
    // 呼叫 API...
    console.log('送出資料', form)
  }
}
</script>

<style scoped>
.field-group { margin-bottom: 12px; }
.error-msg { color: #d93025; font-size: 0.9rem; }
</style>

技巧:利用 reactive 建立的 errors 物件,可在任何地方直接修改,Vue 會自動追蹤變化,省去大量 ref 的寫法。


3️⃣ 全域錯誤捕捉 – errorCaptured 與全域錯誤處理器

有時候錯誤不是由使用者輸入導致,而是 元件渲染或生命週期 中的例外。Vue3 提供 errorCaptured 鉤子讓子元件的錯誤可以被父層捕獲,進一步顯示統一的錯誤 UI。

<!-- ParentComponent.vue -->
<template>
  <div class="app">
    <ErrorBanner v-if="globalError" :message="globalError" @close="globalError = ''" />
    <ChildComponent />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import ErrorBanner from './ErrorBanner.vue'
import ChildComponent from './ChildComponent.vue'

const globalError = ref('')

// 捕獲子元件錯誤
function onError(err, instance, info) {
  console.error('捕獲錯誤:', err, info)
  globalError.value = err.message || '發生未知錯誤'
  // 回傳 false 讓錯誤不再向上冒泡(視需求決定)
  return false
}
</script>

<!-- 使用全域錯誤捕捉 -->
<script>
export default {
  // Vue 3 Options API 寫法
  errorCaptured(err, instance, info) {
    return this.$setupState.onError(err, instance, info)
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <h2>子元件</h2>
    <button @click="triggerError">觸發錯誤</button>
  </div>
</template>

<script setup>
function triggerError() {
  // 故意拋出錯誤,測試 errorCaptured
  throw new Error('子元件執行時發生錯誤')
}
</script>
<!-- ErrorBanner.vue -->
<template>
  <div class="error-banner">
    <span>{{ message }}</span>
    <button @click="$emit('close')">✕</button>
  </div>
</template>

<script setup>
defineProps({
  message: { type: String, required: true }
})
</script>

<style scoped>
.error-banner {
  background: #fdecea;
  color: #b71c1c;
  padding: 12px;
  border-radius: 4px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
</style>

實務建議:將 ErrorBanner 放在根元件(如 App.vue)或布局層,讓所有子頁面共用同一套錯誤顯示樣式,提升使用者體驗。


4️⃣ 自訂錯誤組件 + 插槽 – 彈性化的錯誤呈現

若專案需要 多種錯誤樣式(例如表單錯誤、系統錯誤、網路斷線),可以建立一個通用的 ErrorBox 組件,並透過插槽讓呼叫端自訂內容。

<!-- ErrorBox.vue -->
<template>
  <transition name="fade">
    <div v-if="visible" class="error-box" role="alert">
      <slot name="icon">
        <!-- 預設圖示 -->
        <svg class="icon" viewBox="0 0 24 24"><path d="M12 2a10 10 0 1 0 0 20..." /></svg>
      </slot>
      <div class="msg">
        <slot>{{ message }}</slot>
      </div>
      <button class="close" @click="close">✕</button>
    </div>
  </transition>
</template>

<script setup>
import { ref, watch } from 'vue'

defineProps({
  modelValue: { type: Boolean, default: false },
  message:    { type: String,  default: '' }
})

const visible = ref(false)

watch(() => modelValue, v => (visible.value = v))

function close() {
  visible.value = false
  // 使用 v-model 讓外層同步狀態
  emit('update:modelValue', false)
}
</script>

<style scoped>
.error-box {
  display: flex;
  align-items: center;
  background: #ffebee;
  color: #c62828;
  padding: 10px 16px;
  border-radius: 4px;
  margin: 8px 0;
}
.icon { width: 20px; height: 20px; margin-right: 8px; }
.close { background: transparent; border: none; margin-left: auto; cursor: pointer; }
.fade-enter-active, .fade-leave-active { transition: opacity .3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>

使用方式:

<template>
  <ErrorBox v-model="showError" message="系統暫時無法連線">
    <template #icon>
      <!-- 自訂圖示 -->
      <svg class="icon" viewBox="0 0 24 24"><path d="M12 4a8 8 0 0 1 0 16..." /></svg>
    </template>
    <template #default>
      <strong>網路問題:</strong> 請檢查您的網路連線後再重試。
    </template>
  </ErrorBox>

  <button @click="showError = true">測試錯誤框</button>
</template>

<script setup>
import { ref } from 'vue'
import ErrorBox from '@/components/ErrorBox.vue'

const showError = ref(false)
</script>

優點:錯誤框的外觀與行為完全抽離於業務邏輯,讓 UI 團隊可以獨立調整樣式,開發者只負責傳遞訊息與控制顯示。


5️⃣ 與 TypeScript 搭配的錯誤類型安全

在大型專案中,錯誤型別的統一 能大幅降低因錯誤訊息不一致或拼寫錯誤導致的 UI bug。以下示範如何透過 TypeScript 定義錯誤代碼與對應訊息,並在 template 中使用 computed 產生最終顯示文字。

// errorTypes.ts
export enum ApiErrorCode {
  NETWORK = 'NETWORK',
  UNAUTHORIZED = 'UNAUTHORIZED',
  VALIDATION = 'VALIDATION',
  UNKNOWN = 'UNKNOWN'
}

export const errorMessages: Record<ApiErrorCode, string> = {
  [ApiErrorCode.NETWORK]: '網路連線失敗,請稍後再試',
  [ApiErrorCode.UNAUTHORIZED]: '登入資訊已失效,請重新登入',
  [ApiErrorCode.VALIDATION]: '資料格式錯誤,請檢查輸入內容',
  [ApiErrorCode.UNKNOWN]: '發生未知錯誤,請聯繫客服'
}
<!-- ApiErrorDisplay.vue -->
<template>
  <ErrorBox v-model="visible" :message="displayMessage" />
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ApiErrorCode, errorMessages } from '@/utils/errorTypes'
import ErrorBox from '@/components/ErrorBox.vue'

interface Props {
  errorCode?: ApiErrorCode
}
const props = defineProps<Props>()
const visible = ref(false)

const displayMessage = computed(() => {
  if (!props.errorCode) return ''
  return errorMessages[props.errorCode] ?? errorMessages[ApiErrorCode.UNKNOWN]
})

// 當 errorCode 改變時自動開啟錯誤框
watch(() => props.errorCode, (code) => {
  visible.value = !!code
})
</script>

實務意義:當後端回傳錯誤碼時,只需要把 errorCode 傳給 ApiErrorDisplay,不必在每個元件手動寫字串,保持訊息的一致性與可維護性。


常見陷阱與最佳實踐

陷阱 說明 解決方案
1. 直接在 template 中使用 try/catch Vue 的 template 只能寫表達式,無法放 try/catch,會導致編譯錯誤。 把可能拋錯的程式碼搬到 setup 或 methods 中,並在那裡捕捉後以 ref/reactive 形式回傳錯誤狀態。
2. 錯誤訊息未被響應式追蹤 若直接修改普通物件的屬性(非 reactive),Vue 不會更新 UI。 使用 reactiveref 包裝錯誤物件,或透過 set(Vue2)/Vue.set(Vue3 的 reactive 仍需)確保屬性是響應式的。
3. 多重條件渲染導致 UI 閃爍 同時使用 v-ifv-else-ifv-else 產生頻繁的 DOM 重建。 優先使用 v-show 或將錯誤訊息抽成子組件,減少父層的重繪成本。
4. 全域錯誤捕捉過度使用 把所有錯誤都交給 errorCaptured,會掩蓋本該在 UI 層處理的表單驗證錯誤。 僅在 非預期(如程式錯誤、網路斷線)時使用全域捕捉,表單驗證仍建議使用局部 v-if
5. 錯誤訊息硬寫在組件內 隨著多語系需求或訊息變更,需要大量手動修改。 把錯誤文字抽成 i18n(如 vue-i18n)或錯誤代碼映射表,保持 UI 與文字分離。

最佳實踐總結

  1. 錯誤狀態要響應式ref / reactive 為首選。
  2. 錯誤顯示與業務邏輯分離:使用自訂錯誤組件或插槽。
  3. 盡量統一錯誤訊息來源:錯誤代碼 + i18n / 常量表。
  4. 只在必要時使用全域捕捉,避免把可預期錯誤吞掉。
  5. 為每個錯誤提供可操作的回饋(如「重新整理」或「聯絡客服」按鈕),提升使用者體驗。

實際應用場景

🎯 場景一:表單驗證失敗的即時提示

在電商結帳頁面,使用者必須填寫收件人資訊。若欄位不符合規則,立即在對應輸入框下方顯示錯誤,並在提交時阻止 API 呼叫。

  • 使用 集中管理的 errors 物件,配合 v-for 動態渲染錯誤列表。
  • 搭配 自訂 ErrorBox,讓錯誤樣式與整體 UI 保持一致。

🎯 場景二:API 回傳 401(未授權)時的全域提醒

使用者在多頁面中操作時,若 token 過期,所有 AJAX 請求會回傳 401。此時:

  • Axios 攔截器 捕捉 401,設定全域 authErrortrue
  • 在根層 App.vue 中使用 ErrorBanner,顯示「登入已失效,請重新登入」的訊息並提供「立即前往登入」按鈕。

🎯 場景三:網路斷線或服務不可用的 fallback UI

當使用者的裝置失去網路或伺服器異常,整個 SPA 仍需保持可操作:

  • 利用 全域錯誤捕捉 + ErrorBox,在 errorCaptured 中判斷 err.message 是否為網路相關。
  • 同時在 Vue RouterbeforeEach 中檢查 navigator.onLine,若離線則導向「離線模式」頁面。

🎯 場景四:多語系錯誤訊息的統一管理

大型 SaaS 平台支援中、英、日三種語系:

  • 定義 錯誤代碼ApiErrorCode)與 i18n 資源檔en.jsonzh-TW.jsonja.json)。
  • 在 API 呼叫失敗後,根據回傳的錯誤碼取出當前語系的訊息,交給 ApiErrorDisplay 組件渲染。

總結

在 Vue3 中,錯誤顯示策略不僅是 UI 的細節,更是提升使用者信任與產品品質的關鍵。本文從最基礎的 v-if 條件渲染,延伸到全域錯誤捕捉、自訂錯誤組件、TypeScript 型別安全與多語系管理,提供了 完整、可維護且實務導向的錯誤處理框架

記住

  • 錯誤狀態必須是響應式的,才能保證 UI 即時更新。
  • 錯誤呈現與業務邏輯要分離,使用組件化、插槽或全域錯誤處理器。
  • 統一錯誤訊息來源(代碼 + i18n)能減少維護成本,並支援多語系需求。

透過上述技巧,你可以在任何 Vue3 專案中快速建立 友善、可靠且易於擴充的錯誤顯示機制,讓使用者即使在遇到問題時,也能得到清晰的指引與解決方案。祝開發順利,錯誤處理不再是痛點! 🚀