用 useNuxtApp().payload.error 在 SPA 模式除錯 API 500
在 ssr: false 的 Nuxt 專案裡,API 回傳 500 時 console 一片安靜,卻能透過 useNuxtApp().payload.error 看到完整錯誤內容。本文記錄這個現象的成因,以及純 Vue 專案如何復刻同樣的除錯體驗。
情境
在一個 ssr: false 的 Nuxt 專案除錯時,API 明明回 500,但瀏覽器 console 完全沒有錯誤訊息。抱著死馬當活馬醫的心態打開 DevTools 敲:
useNuxtApp().payload.error
結果完整的錯誤物件就躺在裡面——status、message、甚至 data 欄位裡的 why / fix 都清清楚楚。這才想起這是 Nuxt 預設行為的結果。
為什麼 console 看不到
關鍵在 Nuxt 的 useFetch / useAsyncData:它們預設會把錯誤當作資料吞起來,不丟到 console。
const { data, error } = await useFetch('/api/xxx')
// API 回 500 → error.value 有完整錯誤
// 但 console 一片安靜
這是刻意設計:fetch error 是「狀態」而不是「例外」,交給開發者自己決定要不要顯示。搭配 server 端用 createError({ statusCode, statusMessage, data }) 拋出的結構化錯誤,就會被序列化到 payload.error,變成跨越 server/client 的除錯窗口。
payload.error 在 SPA 也能用嗎
會有點意外,畢竟 ssr: false 聽起來像「沒有 server 渲染,何來 payload」。實際上:
payload不是 SSR 的專屬產物,是 Nuxt 執行期的共用狀態容器showError()/createError()拋出的錯誤會流向payload.erroruseFetch捕獲的 server 端錯誤也會進到這裡- 所以即使純 SPA,Nuxt 仍然幫你統一收錯誤
這就是為什麼 useNuxtApp().payload.error 在 ssr: false 下還是有料可看。
實際使用情境
除了臨時 debug,也能做成常駐的錯誤監看:
<script setup lang="ts">
const nuxtApp = useNuxtApp()
// 開發環境暴露到 window 方便 console 查
if (import.meta.dev) {
watchEffect(() => {
if (nuxtApp.payload.error) {
console.group('[Payload Error]')
console.log(nuxtApp.payload.error)
console.groupEnd()
}
})
}
</script>
純 Vue 怎麼復刻
純 Vue 專案沒有 useNuxtApp(),但同樣的體驗可以靠三個方向做到。
方向一:選用會把 error 存成 ref 的 data fetching library
重點是避免自己寫一堆 try/catch——讓 library 幫你統一接錯誤:
| Library | 等價欄位 |
|---|---|
| Pinia Colada(Vue 官方生態) | useQuery() 的 error |
| TanStack Query (Vue) | useQuery() 的 error |
VueUse useFetch | 回傳 error ref |
import { useFetch } from '@vueuse/core'
const { data, error, statusCode } = useFetch('/api/xxx').json()
// error.value 有完整錯誤,console 不會噴
方向二:自建全域 error store
最接近 payload.error 的體驗——開著 Pinia DevTools 就能即時看所有錯誤。
// stores/debug-errors.ts
export const useDebugErrors = defineStore('debug-errors', () => {
const errors = ref<Array<{ url: string; status?: number; body?: unknown; time: number }>>([])
const last = computed(() => errors.value.at(-1))
function push(entry: Omit<(typeof errors.value)[number], 'time'>) {
errors.value.push({ ...entry, time: Date.now() })
}
return { errors, last, push }
})
搭配一個 fetch wrapper,所有錯誤都會流進 store:
async function safeFetch<T>(url: string, opts?: RequestInit): Promise<T> {
const store = useDebugErrors()
try {
const res = await fetch(url, opts)
if (!res.ok) {
const body = await res.json().catch(() => null)
store.push({ url, status: res.status, body })
throw new Error(`HTTP ${res.status}`)
}
return res.json() as Promise<T>
} catch (err) {
store.push({ url, body: String(err) })
throw err
}
}
方向三:最快速的 window.__ERR__ 暴露
臨時救火用:
const app = createApp(App)
app.config.errorHandler = (err, _instance, info) => {
;(window as any).__ERR__ = { err, info, time: Date.now() }
console.error('[Vue Error]', err, info)
}
window.addEventListener('unhandledrejection', (e) => {
;(window as any).__ERR__ = e.reason
})
之後 DevTools 敲 __ERR__ 就能拿到最近一次錯誤。
關鍵啟示
payload.error 之所以好用,不是因為它是什麼黑魔法,而是 Nuxt 幫你做了兩件事:
- 統一錯誤格式:
createError({ statusCode, statusMessage, data })產出結構化物件 - 統一錯誤通道:所有
useFetch/showError的錯誤都匯流到同一個地方
純 Vue 要得到同樣體驗,別想著去模擬 payload 這個容器——重點在複製那兩件事:API 層統一錯誤 schema + data fetching library 把 error 存成 ref。做到這兩點,useQuery().error 或你自己的 useDebugErrors().last 就是你的 payload.error。
小結
ssr: false下useNuxtApp().payload.error仍然有效,因為payload不是 SSR 專屬- console 沒噴錯是
useFetch把 error 當資料吞起來,不是 bug - 純 Vue 復刻這體驗:選對 data fetching library + 統一 API 錯誤格式,而不是去模擬 payload 本身