Vue 錯誤處理與監控設計
在真實專案裡,錯誤處理不是每個地方寫一個 try/catch 就結束。資深工程師更關心的是:錯誤有沒有被分類、使用者能不能恢復、團隊能不能定位、線上問題有沒有指標、敏感資料會不會被上報出去。
一句話回答:Vue 錯誤處理要分層設計:元件渲染錯誤用 app.config.errorHandler 與 errorCaptured 收斂,非同步與 API 錯誤在請求層統一標準化,路由載入錯誤用 Router hook 補救,全域 Promise 與資源錯誤要監聽上報;監控系統則要帶上 release、route、user、requestId、breadcrumb、Source Map 與隱私過濾,讓線上錯誤能被定位與修復。
一、 必考觀念
1. 錯誤要先分類
面試時不要只回答「用 try/catch」。前端錯誤至少可以分成:
| 類型 | 例子 | 常見處理 |
|---|---|---|
| Vue 渲染錯誤 | template、render、watcher、生命週期內丟錯 | app.config.errorHandler、errorCaptured |
| 元件局部錯誤 | 某個區塊壞掉,不希望整頁白屏 | Error Boundary、fallback UI |
| API 錯誤 | 401、403、500、業務錯誤碼、timeout | API client 統一處理 |
| 非同步錯誤 | Promise reject 沒有 catch | unhandledrejection、局部 catch |
| 路由錯誤 | lazy route chunk 載入失敗、guard reject | router.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 管理範圍內的錯誤。
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:
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 元件
可以包住高風險區塊,例如圖表、第三方元件、低程式碼表單、富文字編輯器。
<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>使用:
<ErrorBoundary>
<HeavyChart />
</ErrorBoundary>重點是局部降級,而不是讓整個頁面白屏。
三、 API 錯誤處理
1. API client 要標準化錯誤
不要讓每個頁面自己解析錯誤。
可以統一成:
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 要分開處理
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
不好的做法:
catch (error) {
toast.error('提交失敗')
}使用者不知道哪個欄位錯。
更好的方式:
try {
await submitForm(form)
} catch (error) {
if (isValidationError(error)) {
fieldErrors.value = error.fields
return
}
toast.error(error.message)
}表單錯誤要盡量回到欄位,系統錯誤才用 toast 或 modal。
四、 非同步與路由錯誤
1. Promise 錯誤要避免漏接
局部非同步流程最好自己處理錯誤:
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
}
}全域也可以補一層:
window.addEventListener('unhandledrejection', event => {
reportError(event.reason, {
source: 'unhandledrejection',
})
})全域監聽是最後防線,不應該取代局部錯誤處理。
2. 路由懶載入錯誤
路由懶載入依賴 JS chunk。如果使用者開著舊頁面,部署新版本後再切路由,可能請求到不存在的舊 chunk。
可以用 router.onError:
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。
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. 上報要帶足上下文
只上報 message 和 stack 常常不夠。
建議帶:
| 欄位 | 作用 |
|---|---|
| message / stack | 錯誤內容與堆疊 |
| release | 發版版本 |
| route | 錯誤發生頁面 |
| userId / tenantId | 受影響範圍 |
| requestId | 對接後端 log |
| browser / os | 環境差異 |
| breadcrumbs | 錯誤前操作路徑 |
| feature flag | 是否和灰度開關有關 |
| build time | 確認使用者載入的資源版本 |
例如:
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 狀態。
範例:
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 要分開
很多頁面只寫:
<div v-if="loading">Loading...</div>
<div v-else>{{ data }}</div>但成熟頁面至少要分:
- 初次載入。
- 背景刷新。
- 載入失敗。
- 空資料。
- 局部錯誤。
- 重試中。
這和資料請求層會互相影響。
2. 錯誤訊息要能讓使用者行動
不好的錯誤訊息:
操作失敗比較好的錯誤訊息:
儲存失敗,請檢查網路後重試。你剛剛輸入的內容已保留。或:
你沒有刪除使用者的權限,請聯絡管理員開通 user:delete。錯誤訊息不一定要暴露技術細節,但要給使用者下一步。
3. 失敗時要保留重要狀態
例如:
- 表單送出失敗不能清空。
- 編輯頁 refresh 失敗可以保留舊資料。
- 樂觀更新失敗要 rollback。
- 批次操作部分失敗要列出失敗項目。
- 離開頁面前要提醒未儲存草稿。
延伸讀:Vue 表單、非同步狀態與資料流設計。
七、 追問題庫
Q1:Vue 專案如何做全域錯誤處理?
可以回答:
- 用
app.config.errorHandler捕捉 Vue 管理範圍內的錯誤。 - 高風險局部區塊用
errorCaptured或 Error Boundary 降級。 - API client 統一標準化錯誤格式。
- 用
window.unhandledrejection補未處理 Promise 錯誤。 - 用
window.error監聽資源錯誤。 - 用
router.onError處理動態 import chunk 失敗。 - 統一上報到監控系統,帶 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 載入失敗要提示重新整理。
- 上報要能定位版本與頁面。
可以這樣拆:
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 回填欄位簡化程式:
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 | 使用者取消請求、表單驗證失敗 | 不告警或只記錄 |
| warning | API 偶發 timeout、資源載入失敗 | 聚合觀察 |
| error | 頁面功能不可用、JS exception | 上報並排查 |
| critical | 大量白屏、登入不可用、支付失敗 | 即時告警 |
告警太多會讓團隊麻木,所以要有聚合、去重與門檻。
2. 監控要和發版流程接起來
成熟流程會包含:
- 每次發版產生 release id。
- Source Map 上傳監控平台。
- 監控事件帶 release。
- 發版後看錯誤率與核心流程成功率。
- 異常時可以回滾或關閉 feature flag。
這樣錯誤處理才不只是前端程式碼,而是工程系統的一部分。
3. 隱私與安全不能忽略
上報資料要避免包含:
- password。
- token。
- 身分證、電話、地址。
- 完整 request body。
- 使用者輸入的敏感內容。
錯誤監控是為了定位問題,不是把使用者資料送出去。
4. 不要把錯誤都吞掉
有些程式會寫:
try {
await riskyTask()
} catch {}這很危險,因為問題消失在表面,但狀態可能已經壞掉。
比較好的做法是:
- 預期內錯誤:轉成使用者可理解的狀態。
- 預期外錯誤:上報,並提供 fallback。
- 不可恢復錯誤:讓流程停止,避免寫入錯資料。
總結
| 問題 | 回答重點 |
|---|---|
| Vue 全域錯誤 | app.config.errorHandler、errorCaptured |
| 局部降級 | 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、密碼與個資。資深一點還要做錯誤分級、去重、告警門檻和回滾策略。