2026年4月18日Nuxt

用 useNuxtApp().payload.error 在 SPA 模式除錯 API 500

在 ssr: false 的 Nuxt 專案裡,API 回傳 500 時 console 一片安靜,卻能透過 useNuxtApp().payload.error 看到完整錯誤內容。本文記錄這個現象的成因,以及純 Vue 專案如何復刻同樣的除錯體驗。

NuxtVueDebugError Handling

情境

在一個 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.error
  • useFetch 捕獲的 server 端錯誤也會進到這裡
  • 所以即使純 SPA,Nuxt 仍然幫你統一收錯誤

這就是為什麼 useNuxtApp().payload.errorssr: 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 幫你做了兩件事:

  1. 統一錯誤格式createError({ statusCode, statusMessage, data }) 產出結構化物件
  2. 統一錯誤通道:所有 useFetch / showError 的錯誤都匯流到同一個地方

純 Vue 要得到同樣體驗,別想著去模擬 payload 這個容器——重點在複製那兩件事:API 層統一錯誤 schema + data fetching library 把 error 存成 ref。做到這兩點,useQuery().error 或你自己的 useDebugErrors().last 就是你的 payload.error

小結

  • ssr: falseuseNuxtApp().payload.error 仍然有效,因為 payload 不是 SSR 專屬
  • console 沒噴錯是 useFetch 把 error 當資料吞起來,不是 bug
  • 純 Vue 復刻這體驗:選對 data fetching library + 統一 API 錯誤格式,而不是去模擬 payload 本身

© 2026 YuDefine · Charles