跳至主要內容
Skip to content

Vue 錯誤處理與監控設計

在真實專案裡,錯誤處理不是每個地方寫一個 try/catch 就結束。資深工程師更關心的是:錯誤有沒有被分類、使用者能不能恢復、團隊能不能定位、線上問題有沒有指標、敏感資料會不會被上報出去。

一句話回答:Vue 錯誤處理要分層設計:元件渲染錯誤用 app.config.errorHandlererrorCaptured 收斂,非同步與 API 錯誤在請求層統一標準化,路由載入錯誤用 Router hook 補救,全域 Promise 與資源錯誤要監聽上報;監控系統則要帶上 release、route、user、requestId、breadcrumb、Source Map 與隱私過濾,讓線上錯誤能被定位與修復。


一、 必考觀念

1. 錯誤要先分類

面試時不要只回答「用 try/catch」。前端錯誤至少可以分成:

類型例子常見處理
Vue 渲染錯誤template、render、watcher、生命週期內丟錯app.config.errorHandlererrorCaptured
元件局部錯誤某個區塊壞掉,不希望整頁白屏Error Boundary、fallback UI
API 錯誤401、403、500、業務錯誤碼、timeoutAPI client 統一處理
非同步錯誤Promise reject 沒有 catchunhandledrejection、局部 catch
路由錯誤lazy route chunk 載入失敗、guard rejectrouter.onError、重新載入或提示
資源錯誤script、style、image 載入失敗window.addEventListener('error', ..., true)
使用者操作錯誤表單驗證、權限不足、資料衝突表單錯誤、toast、modal、重試

分類的目的不是炫技,而是避免所有錯誤都用同一種 UI 和同一種上報方式處理。

2. 錯誤處理有兩個目標

第一個目標是使用者體驗:

  • 不白屏。
  • 能知道發生什麼事。
  • 能重試、返回或保留輸入。
  • 權限與登入錯誤要導到正確頁面。
  • 表單錯誤要回填到欄位。

第二個目標是工程定位:

  • 哪個版本出錯。
  • 哪個頁面出錯。
  • 哪個使用者或租戶受影響。
  • 哪個 API 失敗。
  • 錯誤發生前使用者做了什麼。
  • 是否有 Source Map 能還原原始程式碼位置。

只顧使用者提示,團隊會很難修;只顧上報,使用者會很痛苦。

3. 不同錯誤要有不同恢復策略

錯誤恢復策略
token 過期清登入狀態,導 login,保留 redirect
無權限導 403 或顯示無權限狀態
API timeout顯示重試、保留舊資料
表單欄位錯誤回填欄位,不清空草稿
Chunk 載入失敗提示版本更新,允許重新整理
局部元件錯誤顯示 fallback,不讓整頁崩潰

延伸讀:Vue 權限與路由守衛設計Vue 表單、非同步狀態與資料流設計


二、 Vue 全域錯誤處理

1. app.config.errorHandler

Vue 3 可以用 app.config.errorHandler 捕捉元件渲染、事件處理器、生命週期、watch 等 Vue 管理範圍內的錯誤。

typescript
import type { App } from 'vue'

export function setupVueErrorHandler(app: App) {
  app.config.errorHandler = (error, instance, info) => {
    reportError(error, {
      source: 'vue',
      info,
      component: instance?.type?.name,
    })
  }
}

這適合做:

  • 統一上報。
  • 補充 component name。
  • 補充 Vue error info。
  • 避免線上錯誤完全沒有紀錄。

但它不是萬能的。像資源載入失敗、部分 Promise 未處理錯誤、第三方 script error,就不一定會進到 Vue 的 errorHandler。

2. errorCaptured

errorCaptured 可以在父元件捕捉子孫元件錯誤,適合做局部錯誤邊界。

Options API:

typescript
export default {
  errorCaptured(error, instance, info) {
    reportError(error, {
      source: 'component-boundary',
      info,
      component: instance?.type?.name,
    })

    this.hasError = true

    return false
  },
}

回傳 false 代表阻止錯誤繼續向上傳遞。

3. Error Boundary 元件

可以包住高風險區塊,例如圖表、第三方元件、低程式碼表單、富文字編輯器。

vue
<script setup lang="ts">
import { onErrorCaptured, ref } from 'vue'

const hasError = ref(false)

onErrorCaptured((error, instance, info) => {
  hasError.value = true

  reportError(error, {
    source: 'error-boundary',
    info,
    component: instance?.type?.name,
  })

  return false
})
</script>

<template>
  <section v-if="hasError">
    <p>這個區塊暫時無法顯示。</p>
    <button type="button" @click="hasError = false">
      重試
    </button>
  </section>

  <slot v-else />
</template>

使用:

vue
<ErrorBoundary>
  <HeavyChart />
</ErrorBoundary>

重點是局部降級,而不是讓整個頁面白屏。


三、 API 錯誤處理

1. API client 要標準化錯誤

不要讓每個頁面自己解析錯誤。

可以統一成:

typescript
type AppErrorType =
  | 'network'
  | 'timeout'
  | 'unauthorized'
  | 'forbidden'
  | 'validation'
  | 'business'
  | 'server'
  | 'unknown'

class AppError extends Error {
  type: AppErrorType
  status?: number
  code?: string
  requestId?: string
  details?: unknown

  constructor(message: string, options: Partial<AppError>) {
    super(message)
    this.name = 'AppError'
    Object.assign(this, options)
  }
}

這樣頁面拿到錯誤時,不需要猜測是 HTTP 狀態、業務錯誤碼還是網路錯誤。

2. 401、403、500 要分開處理

typescript
async function request<T>(input: RequestInfo, init?: RequestInit): Promise<T> {
  const response = await fetch(input, init)
  const requestId = response.headers.get('x-request-id') ?? undefined

  if (response.status === 401) {
    throw new AppError('登入已過期', {
      type: 'unauthorized',
      status: 401,
      requestId,
    })
  }

  if (response.status === 403) {
    throw new AppError('沒有操作權限', {
      type: 'forbidden',
      status: 403,
      requestId,
    })
  }

  if (!response.ok) {
    throw new AppError('服務暫時不可用', {
      type: 'server',
      status: response.status,
      requestId,
    })
  }

  return response.json()
}

不同錯誤有不同處理:

  • 401:清 token,導 login。
  • 403:顯示無權限,不一定要登出。
  • 422:回填欄位錯誤。
  • 500:提示重試,並上報。
  • timeout:允許重試或保留舊資料。

延伸讀:Vue 請求、快取與體驗優化

3. 表單錯誤不要只 toast

不好的做法:

typescript
catch (error) {
  toast.error('提交失敗')
}

使用者不知道哪個欄位錯。

更好的方式:

typescript
try {
  await submitForm(form)
} catch (error) {
  if (isValidationError(error)) {
    fieldErrors.value = error.fields
    return
  }

  toast.error(error.message)
}

表單錯誤要盡量回到欄位,系統錯誤才用 toast 或 modal。


四、 非同步與路由錯誤

1. Promise 錯誤要避免漏接

局部非同步流程最好自己處理錯誤:

typescript
const error = ref<Error | null>(null)
const loading = ref(false)

async function loadData() {
  loading.value = true
  error.value = null

  try {
    data.value = await fetchDashboard()
  } catch (err) {
    error.value = normalizeError(err)
    reportError(err, {
      source: 'dashboard',
    })
  } finally {
    loading.value = false
  }
}

全域也可以補一層:

typescript
window.addEventListener('unhandledrejection', event => {
  reportError(event.reason, {
    source: 'unhandledrejection',
  })
})

全域監聽是最後防線,不應該取代局部錯誤處理。

2. 路由懶載入錯誤

路由懶載入依賴 JS chunk。如果使用者開著舊頁面,部署新版本後再切路由,可能請求到不存在的舊 chunk。

可以用 router.onError

typescript
router.onError(error => {
  reportError(error, {
    source: 'router',
  })

  if (/Loading chunk|Failed to fetch dynamically imported module/.test(error.message)) {
    showReloadPrompt()
  }
})

常見處理:

  • 提示使用者重新整理。
  • 重新載入目前頁。
  • 保留未儲存資料時先提醒。
  • 靜態資源使用合理快取與版本策略。

延伸讀:Vue 打包與資源優化

3. 資源載入錯誤

圖片、script、style 載入失敗不一定會進入 Vue errorHandler。

typescript
window.addEventListener(
  'error',
  event => {
    const target = event.target

    if (target instanceof HTMLScriptElement) {
      reportError(new Error('Script load failed'), {
        source: 'resource',
        url: target.src,
      })
    }

    if (target instanceof HTMLImageElement) {
      reportError(new Error('Image load failed'), {
        source: 'resource',
        url: target.src,
      })
    }
  },
  true,
)

這類錯誤通常要搭配資源域名、CDN、版本號一起看。


五、 監控上報設計

1. 上報要帶足上下文

只上報 messagestack 常常不夠。

建議帶:

欄位作用
message / stack錯誤內容與堆疊
release發版版本
route錯誤發生頁面
userId / tenantId受影響範圍
requestId對接後端 log
browser / os環境差異
breadcrumbs錯誤前操作路徑
feature flag是否和灰度開關有關
build time確認使用者載入的資源版本

例如:

typescript
function reportError(error: unknown, context: Record<string, unknown> = {}) {
  const normalized = normalizeError(error)

  monitoring.captureException(normalized, {
    tags: {
      release: import.meta.env.VITE_APP_RELEASE,
      route: router.currentRoute.value.fullPath,
    },
    extra: {
      ...context,
      requestId: getCurrentRequestId(),
      breadcrumbs: getBreadcrumbs(),
    },
  })
}

實務上可以用現成監控平台,也可以先接公司內部 log 系統。重點是格式一致、可查、可聚合。

2. Breadcrumb 很重要

線上錯誤最難的是重現。

Breadcrumb 可以記錄:

  • route change。
  • button click。
  • API request。
  • API response status。
  • store action。
  • feature flag 狀態。

範例:

typescript
addBreadcrumb({
  type: 'route',
  message: 'navigate',
  data: {
    from: from.fullPath,
    to: to.fullPath,
  },
})

這能幫你知道錯誤發生前使用者做了什麼。

3. Source Map 要謹慎管理

線上 JS 通常被壓縮,沒有 Source Map 會很難定位。

但 Source Map 也可能暴露原始碼,所以常見做法是:

  • production 產出 Source Map。
  • 上傳到監控平台。
  • 不公開 Source Map 檔案。
  • 監控事件帶 release。
  • 發版流程確保 release 和 Source Map 對得上。

這是工程化題目,不只是 Vue 題目。


六、 使用者回饋設計

1. Loading、Error、Empty 要分開

很多頁面只寫:

vue
<div v-if="loading">Loading...</div>
<div v-else>{{ data }}</div>

但成熟頁面至少要分:

  • 初次載入。
  • 背景刷新。
  • 載入失敗。
  • 空資料。
  • 局部錯誤。
  • 重試中。

這和資料請求層會互相影響。

2. 錯誤訊息要能讓使用者行動

不好的錯誤訊息:

text
操作失敗

比較好的錯誤訊息:

text
儲存失敗,請檢查網路後重試。你剛剛輸入的內容已保留。

或:

text
你沒有刪除使用者的權限,請聯絡管理員開通 user:delete。

錯誤訊息不一定要暴露技術細節,但要給使用者下一步。

3. 失敗時要保留重要狀態

例如:

  • 表單送出失敗不能清空。
  • 編輯頁 refresh 失敗可以保留舊資料。
  • 樂觀更新失敗要 rollback。
  • 批次操作部分失敗要列出失敗項目。
  • 離開頁面前要提醒未儲存草稿。

延伸讀:Vue 表單、非同步狀態與資料流設計


七、 追問題庫

Q1:Vue 專案如何做全域錯誤處理?

可以回答:

  1. app.config.errorHandler 捕捉 Vue 管理範圍內的錯誤。
  2. 高風險局部區塊用 errorCaptured 或 Error Boundary 降級。
  3. API client 統一標準化錯誤格式。
  4. window.unhandledrejection 補未處理 Promise 錯誤。
  5. window.error 監聽資源錯誤。
  6. router.onError 處理動態 import chunk 失敗。
  7. 統一上報到監控系統,帶 release、route、requestId、breadcrumbs。

Q2:app.config.errorHandler 能捕捉所有錯誤嗎?

不能。

它主要處理 Vue 管理範圍內的錯誤,例如 render、event handler、lifecycle、watcher 等。資源載入錯誤、部分全域 Promise reject、第三方 script error、API 業務錯誤,需要其他層處理。

Q3:錯誤上報要帶哪些資訊?

至少帶:

  • message。
  • stack。
  • route。
  • release。
  • component。
  • userId 或 tenantId。
  • requestId。
  • browser / OS。
  • breadcrumbs。

同時要做敏感資料過濾。

Q4:API 錯誤為什麼要統一封裝?

因為頁面不應該知道每種後端錯誤格式。

統一封裝後,頁面只需要判斷:

  • 是否登入過期。
  • 是否無權限。
  • 是否欄位驗證錯。
  • 是否可重試。
  • 是否要顯示 toast、modal 或 inline error。

這會讓錯誤處理更一致。

Q5:Chunk 載入失敗怎麼辦?

常見於部署新版本後,使用者舊頁面引用舊 chunk。

可以在 router.onError 偵測動態 import 失敗,提示使用者重新整理,或自動刷新。發版與 CDN 快取策略也要配合,避免 HTML 長快取引用到已刪除資源。


八、 實作題

題目:設計 Vue 後台的錯誤處理與監控方案

需求:

  • Vue 元件錯誤要上報。
  • API 錯誤要統一轉換。
  • 401 導 login,403 顯示無權限。
  • 表單錯誤要能回填欄位。
  • chunk 載入失敗要提示重新整理。
  • 上報要能定位版本與頁面。

可以這樣拆:

text
main.ts
  -> setupVueErrorHandler(app)
  -> setupGlobalErrorListeners()

router.ts
  -> beforeEach 做登入與權限
  -> onError 處理 chunk error

apiClient.ts
  -> 統一轉換 AppError
  -> 帶 requestId
  -> 401/403 分流

monitoring.ts
  -> captureException
  -> breadcrumbs
  -> release / route / user context

pages
  -> 局部 loading/error/retry
  -> validation error 回填欄位

簡化程式:

typescript
export function setupErrorHandling(app: App, router: Router) {
  app.config.errorHandler = (error, instance, info) => {
    reportError(error, {
      source: 'vue',
      info,
      component: instance?.type?.name,
    })
  }

  window.addEventListener('unhandledrejection', event => {
    reportError(event.reason, {
      source: 'unhandledrejection',
    })
  })

  router.onError(error => {
    reportError(error, {
      source: 'router',
    })

    if (isChunkLoadError(error)) {
      showReloadPrompt()
    }
  })
}

面試時補一句:這只是收斂錯誤入口,真正的體驗還是要在頁面層設計 loading、error、empty、retry 與 fallback。


九、 資深視角

1. 錯誤處理要有分級

不是所有錯誤都要彈 modal,也不是所有錯誤都要叫醒工程師。

可以分:

等級例子處理
info使用者取消請求、表單驗證失敗不告警或只記錄
warningAPI 偶發 timeout、資源載入失敗聚合觀察
error頁面功能不可用、JS exception上報並排查
critical大量白屏、登入不可用、支付失敗即時告警

告警太多會讓團隊麻木,所以要有聚合、去重與門檻。

2. 監控要和發版流程接起來

成熟流程會包含:

  • 每次發版產生 release id。
  • Source Map 上傳監控平台。
  • 監控事件帶 release。
  • 發版後看錯誤率與核心流程成功率。
  • 異常時可以回滾或關閉 feature flag。

這樣錯誤處理才不只是前端程式碼,而是工程系統的一部分。

3. 隱私與安全不能忽略

上報資料要避免包含:

  • password。
  • token。
  • 身分證、電話、地址。
  • 完整 request body。
  • 使用者輸入的敏感內容。

錯誤監控是為了定位問題,不是把使用者資料送出去。

4. 不要把錯誤都吞掉

有些程式會寫:

typescript
try {
  await riskyTask()
} catch {}

這很危險,因為問題消失在表面,但狀態可能已經壞掉。

比較好的做法是:

  • 預期內錯誤:轉成使用者可理解的狀態。
  • 預期外錯誤:上報,並提供 fallback。
  • 不可恢復錯誤:讓流程停止,避免寫入錯資料。

總結

問題回答重點
Vue 全域錯誤app.config.errorHandlererrorCaptured
局部降級Error Boundary、fallback UI
API 錯誤統一 AppError、401/403/validation 分流
Promise 錯誤局部 catch + unhandledrejection 最後防線
路由錯誤router.onError 處理 lazy chunk 失敗
監控上報release、route、requestId、breadcrumbs、Source Map
資深取捨分級、去重、隱私過濾、告警門檻、發版整合

一句完整的面試回答可以是:

Vue 錯誤處理我會分層做。Vue 管理範圍內的錯誤用 app.config.errorHandler 統一上報,高風險局部區塊用 errorCaptured 或 Error Boundary 做 fallback。API 錯誤在 request client 統一轉成 AppError,401 導 login,403 顯示無權限,validation error 回填表單欄位,timeout 和 server error 提供重試。非同步漏接用 unhandledrejection 補最後防線,路由懶載入失敗用 router.onError 提示重新整理。監控上報要帶 release、route、component、requestId、breadcrumbs,Source Map 要跟發版流程對齊,同時過濾 token、密碼與個資。資深一點還要做錯誤分級、去重、告警門檻和回滾策略。

延伸閱讀